@xleddyl/nuxt-cms 0.9.3 → 0.10.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 (433) hide show
  1. package/README.md +1 -1
  2. package/dist/module.d.mts +20 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +1037 -148
  5. package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +1 -1
  6. package/dist/runtime/app/blocks/CmsBlocks.vue +57 -9
  7. package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +1 -1
  8. package/dist/runtime/app/blocks/CmsField.d.ts +24 -0
  9. package/dist/runtime/app/blocks/CmsField.js +16 -0
  10. package/dist/runtime/app/blocks/node.d.ts +37 -0
  11. package/dist/runtime/app/blocks/node.js +17 -0
  12. package/dist/runtime/app/blocks/resolve.d.ts +18 -0
  13. package/dist/runtime/app/blocks/resolve.js +34 -0
  14. package/dist/runtime/app/components/cms/BlockPicker.d.vue.ts +22 -0
  15. package/dist/runtime/app/components/cms/BlockPicker.vue +87 -0
  16. package/dist/runtime/app/components/cms/BlockPicker.vue.d.ts +22 -0
  17. package/dist/runtime/app/components/cms/BlockPickerPopover.d.vue.ts +38 -0
  18. package/dist/runtime/app/components/cms/BlockPickerPopover.vue +44 -0
  19. package/dist/runtime/app/components/cms/BlockPickerPopover.vue.d.ts +38 -0
  20. package/dist/runtime/app/components/cms/BlockTree.d.vue.ts +29 -0
  21. package/dist/runtime/app/components/cms/BlockTree.vue +322 -0
  22. package/dist/runtime/app/components/cms/BlockTree.vue.d.ts +29 -0
  23. package/dist/runtime/app/components/cms/BlocksField.vue +569 -218
  24. package/dist/runtime/app/components/cms/BrandMark.vue +7 -13
  25. package/dist/runtime/app/components/cms/ConfirmGroups.vue +15 -13
  26. package/dist/runtime/app/components/cms/{BlockPalette.vue.d.ts → EditorHistory.d.vue.ts} +6 -5
  27. package/dist/runtime/app/components/cms/EditorHistory.vue +34 -0
  28. package/dist/runtime/app/components/cms/{BlockPalette.d.vue.ts → EditorHistory.vue.d.ts} +6 -5
  29. package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +11 -5
  30. package/dist/runtime/app/components/cms/EntryDrawer.vue +183 -152
  31. package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +11 -5
  32. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +16 -4
  33. package/dist/runtime/app/components/cms/EntryForm.vue +106 -146
  34. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +16 -4
  35. package/dist/runtime/app/components/cms/FieldInput.vue +10 -2
  36. package/dist/runtime/app/components/cms/FormSections.d.vue.ts +22 -0
  37. package/dist/runtime/app/components/cms/FormSections.vue +71 -0
  38. package/dist/runtime/app/components/cms/FormSections.vue.d.ts +22 -0
  39. package/dist/runtime/app/components/cms/ImportModal.vue +46 -58
  40. package/dist/runtime/app/components/cms/LinkField.d.vue.ts +16 -0
  41. package/dist/runtime/app/components/cms/LinkField.vue +250 -0
  42. package/dist/runtime/app/components/cms/LinkField.vue.d.ts +16 -0
  43. package/dist/runtime/app/components/cms/ListFilters.vue +10 -12
  44. package/dist/runtime/app/components/cms/LocaleSwitch.d.vue.ts +2 -1
  45. package/dist/runtime/app/components/cms/LocaleSwitch.vue +29 -5
  46. package/dist/runtime/app/components/cms/LocaleSwitch.vue.d.ts +2 -1
  47. package/dist/runtime/app/components/cms/MediaDetail.d.vue.ts +2 -0
  48. package/dist/runtime/app/components/cms/MediaDetail.vue +112 -83
  49. package/dist/runtime/app/components/cms/MediaDetail.vue.d.ts +2 -0
  50. package/dist/runtime/app/components/cms/MediaField.vue +35 -25
  51. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +26 -71
  52. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +1 -0
  53. package/dist/runtime/app/components/cms/MediaGallery.vue +442 -419
  54. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +1 -0
  55. package/dist/runtime/app/components/cms/MediaThumb.vue +9 -25
  56. package/dist/runtime/app/components/cms/MediaUpload.vue +1 -1
  57. package/dist/runtime/app/components/cms/{BlockOutline.d.vue.ts → NestedBlockList.d.vue.ts} +5 -5
  58. package/dist/runtime/app/components/cms/NestedBlockList.vue +87 -0
  59. package/dist/runtime/app/components/cms/{BlockOutline.vue.d.ts → NestedBlockList.vue.d.ts} +5 -5
  60. package/dist/runtime/app/components/cms/PoweredBy.vue +12 -5
  61. package/dist/runtime/app/components/cms/PresetModal.d.vue.ts +18 -0
  62. package/dist/runtime/app/components/cms/PresetModal.vue +70 -0
  63. package/dist/runtime/app/components/cms/PresetModal.vue.d.ts +18 -0
  64. package/dist/runtime/app/components/cms/RelationField.vue +2 -8
  65. package/dist/runtime/app/components/cms/SettingsAccount.vue +60 -71
  66. package/dist/runtime/app/components/cms/SettingsColumns.vue +47 -69
  67. package/dist/runtime/app/components/cms/SettingsEmptyAdd.d.vue.ts +13 -0
  68. package/dist/runtime/app/components/cms/SettingsEmptyAdd.vue +21 -0
  69. package/dist/runtime/app/components/cms/SettingsEmptyAdd.vue.d.ts +13 -0
  70. package/dist/runtime/app/components/cms/SettingsEntries.d.vue.ts +13 -0
  71. package/dist/runtime/app/components/cms/SettingsEntries.vue +50 -0
  72. package/dist/runtime/app/components/cms/SettingsEntries.vue.d.ts +13 -0
  73. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +1 -10
  74. package/dist/runtime/app/components/cms/SettingsEntry.vue +11 -95
  75. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +1 -10
  76. package/dist/runtime/app/components/cms/SettingsEntryActions.d.vue.ts +7 -0
  77. package/dist/runtime/app/components/cms/SettingsEntryActions.vue +42 -0
  78. package/dist/runtime/app/components/cms/SettingsEntryActions.vue.d.ts +7 -0
  79. package/dist/runtime/app/components/cms/SettingsEntryTabs.d.vue.ts +7 -0
  80. package/dist/runtime/app/components/cms/SettingsEntryTabs.vue +21 -0
  81. package/dist/runtime/app/components/cms/SettingsEntryTabs.vue.d.ts +7 -0
  82. package/dist/runtime/app/components/cms/SettingsFormAction.d.vue.ts +3 -0
  83. package/dist/runtime/app/components/cms/SettingsFormAction.vue +17 -0
  84. package/dist/runtime/app/components/cms/SettingsFormAction.vue.d.ts +3 -0
  85. package/dist/runtime/app/components/cms/SettingsLayout.vue +18 -14
  86. package/dist/runtime/app/components/cms/SettingsPresetForm.d.vue.ts +11 -0
  87. package/dist/runtime/app/components/cms/SettingsPresetForm.vue +60 -0
  88. package/dist/runtime/app/components/cms/SettingsPresetForm.vue.d.ts +11 -0
  89. package/dist/runtime/app/components/cms/SettingsPresets.d.vue.ts +13 -0
  90. package/dist/runtime/app/components/cms/SettingsPresets.vue +65 -0
  91. package/dist/runtime/app/components/cms/SettingsPresets.vue.d.ts +13 -0
  92. package/dist/runtime/app/components/cms/SettingsRecordActions.d.vue.ts +32 -0
  93. package/dist/runtime/app/components/cms/SettingsRecordActions.vue +45 -0
  94. package/dist/runtime/app/components/cms/SettingsRecordActions.vue.d.ts +32 -0
  95. package/dist/runtime/app/components/cms/SettingsRedirectActions.d.vue.ts +3 -0
  96. package/dist/runtime/app/components/cms/SettingsRedirectActions.vue +59 -0
  97. package/dist/runtime/app/components/cms/SettingsRedirectActions.vue.d.ts +3 -0
  98. package/dist/runtime/app/components/cms/SettingsRedirectForm.d.vue.ts +11 -0
  99. package/dist/runtime/app/components/cms/SettingsRedirectForm.vue +77 -0
  100. package/dist/runtime/app/components/cms/SettingsRedirectForm.vue.d.ts +11 -0
  101. package/dist/runtime/app/components/cms/SettingsRedirects.d.vue.ts +13 -0
  102. package/dist/runtime/app/components/cms/SettingsRedirects.vue +117 -0
  103. package/dist/runtime/app/components/cms/SettingsRedirects.vue.d.ts +13 -0
  104. package/dist/runtime/app/components/cms/SettingsSecretCard.d.vue.ts +17 -0
  105. package/dist/runtime/app/components/cms/SettingsSecretCard.vue +43 -0
  106. package/dist/runtime/app/components/cms/SettingsSecretCard.vue.d.ts +17 -0
  107. package/dist/runtime/app/components/cms/SettingsSite.d.vue.ts +3 -0
  108. package/dist/runtime/app/components/cms/SettingsSite.vue +140 -0
  109. package/dist/runtime/app/components/cms/SettingsSite.vue.d.ts +3 -0
  110. package/dist/runtime/app/components/cms/SettingsUserCreate.d.vue.ts +8 -0
  111. package/dist/runtime/app/components/cms/SettingsUserCreate.vue +82 -0
  112. package/dist/runtime/app/components/cms/SettingsUserCreate.vue.d.ts +8 -0
  113. package/dist/runtime/app/components/cms/SettingsUserDetail.d.vue.ts +11 -0
  114. package/dist/runtime/app/components/cms/SettingsUserDetail.vue +179 -0
  115. package/dist/runtime/app/components/cms/SettingsUserDetail.vue.d.ts +11 -0
  116. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +9 -6
  117. package/dist/runtime/app/components/cms/SettingsUsers.vue +88 -234
  118. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +9 -6
  119. package/dist/runtime/app/components/cms/SharePreview.d.vue.ts +18 -0
  120. package/dist/runtime/app/components/cms/SharePreview.vue +219 -0
  121. package/dist/runtime/app/components/cms/SharePreview.vue.d.ts +18 -0
  122. package/dist/runtime/app/components/cms/SlugField.vue +5 -3
  123. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -8
  124. package/dist/runtime/app/components/cms/TranslationBadges.d.vue.ts +7 -0
  125. package/dist/runtime/app/components/cms/TranslationBadges.vue +45 -0
  126. package/dist/runtime/app/components/cms/TranslationBadges.vue.d.ts +7 -0
  127. package/dist/runtime/app/components/cms/TranslationMenu.d.vue.ts +17 -0
  128. package/dist/runtime/app/components/cms/TranslationMenu.vue +28 -0
  129. package/dist/runtime/app/components/cms/TranslationMenu.vue.d.ts +17 -0
  130. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +22 -4
  131. package/dist/runtime/app/components/cms/VisualEditor.vue +1152 -335
  132. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +22 -4
  133. package/dist/runtime/app/components/cms/WelcomeModal.vue +32 -50
  134. package/dist/runtime/app/composables/cms-clipboard.d.ts +8 -0
  135. package/dist/runtime/app/composables/cms-clipboard.js +38 -0
  136. package/dist/runtime/app/composables/cms-confirm.d.ts +6 -8
  137. package/dist/runtime/app/composables/cms-confirm.js +5 -7
  138. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +5 -1
  139. package/dist/runtime/app/composables/cms-entry-disabled.js +7 -0
  140. package/dist/runtime/app/composables/cms-entry-form.d.ts +15 -0
  141. package/dist/runtime/app/composables/cms-entry-form.js +40 -0
  142. package/dist/runtime/app/composables/cms-entry-settings.d.ts +30 -0
  143. package/dist/runtime/app/composables/cms-entry-settings.js +122 -0
  144. package/dist/runtime/app/composables/cms-entry.d.ts +5 -1
  145. package/dist/runtime/app/composables/cms-entry.js +36 -11
  146. package/dist/runtime/app/composables/cms-history.d.ts +10 -0
  147. package/dist/runtime/app/composables/cms-history.js +24 -0
  148. package/dist/runtime/app/composables/cms-link-targets.d.ts +10 -0
  149. package/dist/runtime/app/composables/cms-link-targets.js +40 -0
  150. package/dist/runtime/app/composables/cms-link-usage.d.ts +1 -0
  151. package/dist/runtime/app/composables/cms-link-usage.js +45 -0
  152. package/dist/runtime/app/composables/cms-media-library.d.ts +19 -9
  153. package/dist/runtime/app/composables/cms-media-library.js +31 -13
  154. package/dist/runtime/app/composables/cms-media-uploader.js +6 -20
  155. package/dist/runtime/app/composables/cms-permissions.d.ts +18 -0
  156. package/dist/runtime/app/composables/cms-permissions.js +54 -0
  157. package/dist/runtime/app/composables/cms-presets.d.ts +11 -0
  158. package/dist/runtime/app/composables/cms-presets.js +40 -0
  159. package/dist/runtime/app/composables/cms-preview-mode.d.ts +4 -0
  160. package/dist/runtime/app/composables/cms-preview-mode.js +8 -0
  161. package/dist/runtime/app/composables/cms-query.d.ts +3 -0
  162. package/dist/runtime/app/composables/cms-query.js +45 -20
  163. package/dist/runtime/app/composables/cms-record-list.d.ts +31 -0
  164. package/dist/runtime/app/composables/cms-record-list.js +75 -0
  165. package/dist/runtime/app/composables/cms-route.d.ts +6 -0
  166. package/dist/runtime/app/composables/cms-route.js +36 -0
  167. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  168. package/dist/runtime/app/composables/cms-save-handoff.d.ts +4 -0
  169. package/dist/runtime/app/composables/cms-save-handoff.js +19 -0
  170. package/dist/runtime/app/composables/cms-seo.d.ts +12 -0
  171. package/dist/runtime/app/composables/cms-seo.js +69 -0
  172. package/dist/runtime/app/composables/cms-settings-form.d.ts +13 -0
  173. package/dist/runtime/app/composables/cms-settings-form.js +29 -0
  174. package/dist/runtime/app/composables/cms-settings-sections.d.ts +26 -0
  175. package/dist/runtime/app/composables/cms-settings-sections.js +181 -0
  176. package/dist/runtime/app/composables/cms-settings-stores.d.ts +52 -0
  177. package/dist/runtime/app/composables/cms-settings-stores.js +156 -0
  178. package/dist/runtime/app/composables/cms-settings.d.ts +3 -3
  179. package/dist/runtime/app/composables/cms-settings.js +14 -8
  180. package/dist/runtime/app/composables/cms-site-disabled.d.ts +7 -0
  181. package/dist/runtime/app/composables/cms-site-disabled.js +8 -0
  182. package/dist/runtime/app/composables/cms-site.d.ts +7 -0
  183. package/dist/runtime/app/composables/cms-site.js +22 -0
  184. package/dist/runtime/app/composables/cms-toast.d.ts +2 -8
  185. package/dist/runtime/app/composables/cms-toast.js +3 -9
  186. package/dist/runtime/app/composables/cms-translation-actions.d.ts +15 -0
  187. package/dist/runtime/app/composables/cms-translation-actions.js +108 -0
  188. package/dist/runtime/app/css-url.d.ts +4 -0
  189. package/dist/runtime/app/image/CmsImage.d.vue.ts +89 -0
  190. package/dist/runtime/app/image/CmsImage.vue +91 -0
  191. package/dist/runtime/app/image/CmsImage.vue.d.ts +89 -0
  192. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  193. package/dist/runtime/app/layouts/cms-admin.vue +98 -76
  194. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  195. package/dist/runtime/app/layouts/cms-editor.vue +7 -2
  196. package/dist/runtime/app/links/CmsLink.d.vue.ts +18 -0
  197. package/dist/runtime/app/links/CmsLink.vue +18 -0
  198. package/dist/runtime/app/links/CmsLink.vue.d.ts +18 -0
  199. package/dist/runtime/app/middleware/cms-auth.js +13 -1
  200. package/dist/runtime/app/pages/admin-collection.vue +473 -297
  201. package/dist/runtime/app/pages/admin-content.vue +190 -53
  202. package/dist/runtime/app/pages/admin-entry.vue +79 -75
  203. package/dist/runtime/app/pages/admin-index.vue +18 -5
  204. package/dist/runtime/app/pages/admin-login.vue +1 -1
  205. package/dist/runtime/app/pages/admin-logout.vue +1 -1
  206. package/dist/runtime/app/pages/admin-media.vue +3 -3
  207. package/dist/runtime/app/pages/admin-preview.vue +112 -24
  208. package/dist/runtime/app/plugins/cms-field.d.ts +2 -0
  209. package/dist/runtime/app/plugins/cms-field.js +8 -0
  210. package/dist/runtime/app/plugins/cms-preview-banner.client.d.ts +2 -0
  211. package/dist/runtime/app/plugins/cms-preview-banner.client.js +19 -0
  212. package/dist/runtime/app/plugins/cms-preview.d.ts +2 -0
  213. package/dist/runtime/app/plugins/cms-preview.js +27 -0
  214. package/dist/runtime/app/preview/CmsPreviewBanner.d.vue.ts +7 -0
  215. package/dist/runtime/app/preview/CmsPreviewBanner.vue +63 -0
  216. package/dist/runtime/app/preview/CmsPreviewBanner.vue.d.ts +7 -0
  217. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +17 -3
  218. package/dist/runtime/app/preview/PreviewOverlay.vue +629 -166
  219. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +17 -3
  220. package/dist/runtime/app/preview/field-marker.d.ts +12 -0
  221. package/dist/runtime/app/preview/field-marker.js +20 -0
  222. package/dist/runtime/app/preview/inline-text.d.ts +15 -0
  223. package/dist/runtime/app/preview/inline-text.js +158 -0
  224. package/dist/runtime/app/utils/block-editor.d.ts +22 -0
  225. package/dist/runtime/app/utils/block-editor.js +12 -0
  226. package/dist/runtime/app/utils/keyboard.d.ts +6 -0
  227. package/dist/runtime/app/utils/keyboard.js +52 -0
  228. package/dist/runtime/app/utils/redirects.d.ts +6 -0
  229. package/dist/runtime/app/utils/redirects.js +7 -0
  230. package/dist/runtime/app/utils/ui.d.ts +8 -0
  231. package/dist/runtime/app/utils/ui.js +15 -1
  232. package/dist/runtime/app/utils/users.d.ts +15 -0
  233. package/dist/runtime/app/utils/users.js +14 -0
  234. package/dist/runtime/assets/cms.css +1 -1
  235. package/dist/runtime/generate.js +16 -2
  236. package/dist/runtime/server/api/collection-export.get.js +3 -2
  237. package/dist/runtime/server/api/collection-import.post.js +39 -11
  238. package/dist/runtime/server/api/collection.get.js +29 -10
  239. package/dist/runtime/server/api/collection.post.js +28 -11
  240. package/dist/runtime/server/api/collection.put.js +35 -13
  241. package/dist/runtime/server/api/item.delete.js +19 -7
  242. package/dist/runtime/server/api/item.get.js +3 -2
  243. package/dist/runtime/server/api/item.put.js +52 -18
  244. package/dist/runtime/server/api/link-usage.post.d.ts +5 -0
  245. package/dist/runtime/server/api/link-usage.post.js +21 -0
  246. package/dist/runtime/server/api/media-delete.post.js +4 -2
  247. package/dist/runtime/server/api/media-download.get.js +2 -2
  248. package/dist/runtime/server/api/media-folder.delete.js +8 -2
  249. package/dist/runtime/server/api/media-folder.patch.js +10 -2
  250. package/dist/runtime/server/api/media-folder.post.js +4 -2
  251. package/dist/runtime/server/api/media-move.post.js +7 -4
  252. package/dist/runtime/server/api/media-presign.post.js +2 -2
  253. package/dist/runtime/server/api/media-upload.put.js +2 -2
  254. package/dist/runtime/server/api/media-usage.post.d.ts +2 -4
  255. package/dist/runtime/server/api/media-usage.post.js +13 -9
  256. package/dist/runtime/server/api/media.delete.js +4 -2
  257. package/dist/runtime/server/api/media.get.js +22 -7
  258. package/dist/runtime/server/api/media.post.js +4 -2
  259. package/dist/runtime/server/api/media.put.js +33 -16
  260. package/dist/runtime/server/api/preset.delete.d.ts +4 -0
  261. package/dist/runtime/server/api/preset.delete.js +9 -0
  262. package/dist/runtime/server/api/preset.put.d.ts +3 -0
  263. package/dist/runtime/server/api/preset.put.js +23 -0
  264. package/dist/runtime/server/api/presets-field.get.d.ts +3 -0
  265. package/dist/runtime/server/api/presets-field.get.js +13 -0
  266. package/dist/runtime/server/api/presets-field.post.d.ts +3 -0
  267. package/dist/runtime/server/api/presets-field.post.js +36 -0
  268. package/dist/runtime/server/api/presets.get.d.ts +3 -0
  269. package/dist/runtime/server/api/presets.get.js +7 -0
  270. package/dist/runtime/server/api/preview-link.delete.d.ts +4 -0
  271. package/dist/runtime/server/api/preview-link.delete.js +15 -0
  272. package/dist/runtime/server/api/preview-links.get.d.ts +3 -0
  273. package/dist/runtime/server/api/preview-links.get.js +10 -0
  274. package/dist/runtime/server/api/preview-links.post.d.ts +3 -0
  275. package/dist/runtime/server/api/preview-links.post.js +17 -0
  276. package/dist/runtime/server/api/preview.get.d.ts +3 -0
  277. package/dist/runtime/server/api/preview.get.js +7 -0
  278. package/dist/runtime/server/api/redirect.delete.d.ts +4 -0
  279. package/dist/runtime/server/api/redirect.delete.js +10 -0
  280. package/dist/runtime/server/api/redirect.put.d.ts +3 -0
  281. package/dist/runtime/server/api/redirect.put.js +8 -0
  282. package/dist/runtime/server/api/redirects-import.post.d.ts +3 -0
  283. package/dist/runtime/server/api/redirects-import.post.js +13 -0
  284. package/dist/runtime/server/api/redirects.get.d.ts +3 -0
  285. package/dist/runtime/server/api/redirects.get.js +7 -0
  286. package/dist/runtime/server/api/redirects.post.d.ts +3 -0
  287. package/dist/runtime/server/api/redirects.post.js +7 -0
  288. package/dist/runtime/server/api/search.get.js +8 -3
  289. package/dist/runtime/server/api/settings.delete.js +2 -2
  290. package/dist/runtime/server/api/settings.get.js +4 -2
  291. package/dist/runtime/server/api/settings.put.js +2 -2
  292. package/dist/runtime/server/api/site.get.d.ts +3 -0
  293. package/dist/runtime/server/api/site.get.js +7 -0
  294. package/dist/runtime/server/api/site.put.d.ts +3 -0
  295. package/dist/runtime/server/api/site.put.js +16 -0
  296. package/dist/runtime/server/api/tags.get.js +3 -2
  297. package/dist/runtime/server/api/user.delete.js +8 -2
  298. package/dist/runtime/server/api/user.put.js +14 -4
  299. package/dist/runtime/server/api/users.get.js +2 -2
  300. package/dist/runtime/server/api/users.post.js +8 -4
  301. package/dist/runtime/server/middleware/hidden-site.d.ts +2 -0
  302. package/dist/runtime/server/middleware/hidden-site.js +8 -0
  303. package/dist/runtime/server/middleware/preview-links.d.ts +2 -0
  304. package/dist/runtime/server/middleware/preview-links.js +6 -0
  305. package/dist/runtime/server/middleware/redirects.d.ts +2 -0
  306. package/dist/runtime/server/middleware/redirects.js +18 -0
  307. package/dist/runtime/server/plugins/session-validate.js +4 -1
  308. package/dist/runtime/server/plugins/webhooks.d.ts +3 -0
  309. package/dist/runtime/server/plugins/webhooks.js +27 -0
  310. package/dist/runtime/server/routes/graphql.js +5 -3
  311. package/dist/runtime/server/routes/robots.d.ts +2 -0
  312. package/dist/runtime/server/routes/robots.js +16 -0
  313. package/dist/runtime/server/routes/sitemap.d.ts +2 -0
  314. package/dist/runtime/server/routes/sitemap.js +67 -0
  315. package/dist/runtime/server/utils/cached-value.d.ts +5 -0
  316. package/dist/runtime/server/utils/cached-value.js +24 -0
  317. package/dist/runtime/server/utils/content-routes.d.ts +5 -0
  318. package/dist/runtime/server/utils/content-routes.js +29 -0
  319. package/dist/runtime/server/utils/field-rules.d.ts +19 -0
  320. package/dist/runtime/server/utils/field-rules.js +72 -0
  321. package/dist/runtime/server/utils/graphql.d.ts +3 -1
  322. package/dist/runtime/server/utils/graphql.js +145 -29
  323. package/dist/runtime/server/utils/hooks.d.ts +14 -0
  324. package/dist/runtime/server/utils/hooks.js +71 -0
  325. package/dist/runtime/server/utils/import.d.ts +7 -2
  326. package/dist/runtime/server/utils/import.js +108 -14
  327. package/dist/runtime/server/utils/link-usage.d.ts +4 -0
  328. package/dist/runtime/server/utils/link-usage.js +28 -0
  329. package/dist/runtime/server/utils/media-references.d.ts +6 -1
  330. package/dist/runtime/server/utils/media-references.js +49 -20
  331. package/dist/runtime/server/utils/media-usage.d.ts +2 -0
  332. package/dist/runtime/server/utils/media-usage.js +5 -3
  333. package/dist/runtime/server/utils/page-rows.d.ts +2 -0
  334. package/dist/runtime/server/utils/page-rows.js +42 -29
  335. package/dist/runtime/server/utils/permissions.d.ts +14 -0
  336. package/dist/runtime/server/utils/permissions.js +43 -0
  337. package/dist/runtime/server/utils/presets.d.ts +18 -0
  338. package/dist/runtime/server/utils/presets.js +68 -0
  339. package/dist/runtime/server/utils/preview-links.d.ts +207 -0
  340. package/dist/runtime/server/utils/preview-links.js +161 -0
  341. package/dist/runtime/server/utils/redirects.d.ts +31 -0
  342. package/dist/runtime/server/utils/redirects.js +240 -0
  343. package/dist/runtime/server/utils/registry.js +4 -1
  344. package/dist/runtime/server/utils/relations.d.ts +8 -0
  345. package/dist/runtime/server/utils/relations.js +25 -6
  346. package/dist/runtime/server/utils/require-admin.d.ts +0 -1
  347. package/dist/runtime/server/utils/require-admin.js +4 -8
  348. package/dist/runtime/server/utils/search.d.ts +1 -0
  349. package/dist/runtime/server/utils/search.js +44 -19
  350. package/dist/runtime/server/utils/single-row.d.ts +3 -0
  351. package/dist/runtime/server/utils/single-row.js +4 -0
  352. package/dist/runtime/server/utils/site.d.ts +23 -0
  353. package/dist/runtime/server/utils/site.js +79 -0
  354. package/dist/runtime/server/utils/sitemap.d.ts +12 -0
  355. package/dist/runtime/server/utils/sitemap.js +109 -0
  356. package/dist/runtime/server/utils/user-management.d.ts +4 -0
  357. package/dist/runtime/server/utils/user-management.js +20 -0
  358. package/dist/runtime/server/utils/users.d.ts +1 -1
  359. package/dist/runtime/server/utils/users.js +1 -1
  360. package/dist/runtime/server/utils/visibility.d.ts +1 -0
  361. package/dist/runtime/server/utils/visibility.js +6 -1
  362. package/dist/runtime/server/utils/webhooks.d.ts +24 -0
  363. package/dist/runtime/server/utils/webhooks.js +90 -0
  364. package/dist/runtime/shared/block-paths.d.ts +66 -0
  365. package/dist/runtime/shared/block-paths.js +310 -0
  366. package/dist/runtime/shared/block-picker.d.ts +18 -0
  367. package/dist/runtime/shared/block-picker.js +31 -0
  368. package/dist/runtime/shared/block-signature.d.ts +4 -0
  369. package/dist/runtime/shared/block-signature.js +46 -0
  370. package/dist/runtime/shared/clipboard.d.ts +31 -0
  371. package/dist/runtime/shared/clipboard.js +206 -0
  372. package/dist/runtime/shared/conditions.d.ts +36 -0
  373. package/dist/runtime/shared/conditions.js +100 -0
  374. package/dist/runtime/shared/content-routes.d.ts +42 -0
  375. package/dist/runtime/shared/content-routes.js +237 -0
  376. package/dist/runtime/shared/duplicate.js +2 -1
  377. package/dist/runtime/shared/editor-layout.d.ts +18 -0
  378. package/dist/runtime/shared/editor-layout.js +9 -0
  379. package/dist/runtime/shared/field-options.d.ts +21 -0
  380. package/dist/runtime/shared/field-options.js +90 -0
  381. package/dist/runtime/shared/graphql-sdl.d.ts +1 -0
  382. package/dist/runtime/shared/graphql-sdl.js +50 -26
  383. package/dist/runtime/shared/history.d.ts +27 -0
  384. package/dist/runtime/shared/history.js +123 -0
  385. package/dist/runtime/shared/hooks.d.ts +45 -0
  386. package/dist/runtime/shared/hooks.js +28 -0
  387. package/dist/runtime/shared/index.d.ts +90 -21
  388. package/dist/runtime/shared/index.js +121 -37
  389. package/dist/runtime/shared/inline-text.d.ts +38 -0
  390. package/dist/runtime/shared/inline-text.js +85 -0
  391. package/dist/runtime/shared/layout.d.ts +2 -1
  392. package/dist/runtime/shared/layout.js +3 -0
  393. package/dist/runtime/shared/links.d.ts +48 -0
  394. package/dist/runtime/shared/links.js +155 -0
  395. package/dist/runtime/shared/list-filters.d.ts +2 -0
  396. package/dist/runtime/shared/list-filters.js +2 -0
  397. package/dist/runtime/shared/media-focal.d.ts +30 -0
  398. package/dist/runtime/shared/media-focal.js +52 -0
  399. package/dist/runtime/shared/migrations-dir.d.ts +14 -0
  400. package/dist/runtime/shared/migrations-dir.js +48 -2
  401. package/dist/runtime/shared/presets.d.ts +67 -0
  402. package/dist/runtime/shared/presets.js +127 -0
  403. package/dist/runtime/shared/preview-links.d.ts +64 -0
  404. package/dist/runtime/shared/preview-links.js +54 -0
  405. package/dist/runtime/shared/preview.d.ts +124 -21
  406. package/dist/runtime/shared/preview.js +419 -81
  407. package/dist/runtime/shared/redirects.d.ts +75 -0
  408. package/dist/runtime/shared/redirects.js +230 -0
  409. package/dist/runtime/shared/roles.d.ts +57 -0
  410. package/dist/runtime/shared/roles.js +293 -0
  411. package/dist/runtime/shared/save-state.d.ts +8 -0
  412. package/dist/runtime/shared/save-state.js +15 -0
  413. package/dist/runtime/shared/search.d.ts +16 -1
  414. package/dist/runtime/shared/search.js +41 -7
  415. package/dist/runtime/shared/seo.d.ts +167 -0
  416. package/dist/runtime/shared/seo.js +335 -0
  417. package/dist/runtime/shared/transfer.js +2 -1
  418. package/dist/runtime/shared/translations.d.ts +29 -0
  419. package/dist/runtime/shared/translations.js +123 -0
  420. package/dist/runtime/shared/users.d.ts +1 -1
  421. package/dist/runtime/shared/validation.d.ts +23 -1
  422. package/dist/runtime/shared/validation.js +214 -64
  423. package/dist/runtime/webhooks.d.ts +33 -0
  424. package/dist/runtime/webhooks.js +57 -0
  425. package/dist/types.d.mts +5 -1
  426. package/package.json +6 -2
  427. package/dist/runtime/app/components/cms/BlockOutline.vue +0 -88
  428. package/dist/runtime/app/components/cms/BlockPalette.vue +0 -31
  429. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +0 -12
  430. package/dist/runtime/app/components/cms/SettingsModal.vue +0 -126
  431. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +0 -12
  432. package/dist/runtime/app/composables/cms-field-focus.d.ts +0 -4
  433. package/dist/runtime/app/composables/cms-field-focus.js +0 -20
@@ -1,248 +1,242 @@
1
1
  <template>
2
- <div class="cms-page" :class="{ 'is-fill': isCollectionKind(config) && !error }">
2
+ <div v-if="config.kind === 'single'" class="k-page">
3
3
  <KPageHeader
4
4
  :title="config.label"
5
5
  :icon="cmsIcon(kindMeta.icon)"
6
6
  :description="kindMeta.description"
7
7
  >
8
- <template v-if="config.kind !== 'single' && total" #badge>
9
- <span :key="total" class="cms-count">{{ total }}</span>
10
- </template>
11
- <template v-if="config.kind === 'single'" #actions>
12
- <KButton type="submit" :form="FORM_ID" label="Save" :loading="saving" />
8
+ <template v-if="!singleLock" #actions>
9
+ <CmsTranslationMenu v-model="formState" :fields="config.fields" size="sm" />
10
+ <KButton type="submit" :form="FORM_ID" label="Save" size="sm" :loading="saving" />
13
11
  </template>
14
12
  </KPageHeader>
15
13
 
16
- <KEmptyState
17
- v-if="error"
18
- :icon="cmsIcon('exclamation-triangle')"
19
- title="Could not load entries"
20
- >
21
- <template #actions>
22
- <KButton
23
- label="Retry"
24
- :icon="cmsIcon('arrow-path')"
25
- variant="soft"
26
- :loading="status === 'pending'"
27
- @click="reload"
28
- />
29
- </template>
30
- </KEmptyState>
14
+ <KCard v-if="error" padding="lg" role="alert" fill>
15
+ <KEmptyState
16
+ variant="plain"
17
+ :icon="cmsIcon('exclamation-triangle')"
18
+ title="Could not load entry"
19
+ :description="errorMessage(error)"
20
+ :level="2"
21
+ fill
22
+ >
23
+ <template #actions>
24
+ <KButton
25
+ label="Retry"
26
+ :icon="cmsIcon('arrow-path')"
27
+ variant="soft"
28
+ tone="neutral"
29
+ size="sm"
30
+ :loading="status === 'pending'"
31
+ @click="reload"
32
+ />
33
+ </template>
34
+ </KEmptyState>
35
+ </KCard>
31
36
 
32
- <KCard v-else-if="config.kind === 'single'" variant="outline" padding="lg">
37
+ <KCard v-else padding="lg" fill>
33
38
  <CmsEntryForm
39
+ ref="singleForm"
34
40
  v-model="formState"
35
41
  :fields="config.fields"
36
42
  :tabs="config.tabs"
37
43
  :entry-name="name"
38
44
  :form-id="FORM_ID"
39
- :loading="saving"
45
+ :tab-query="CMS_TAB_QUERY"
40
46
  :focus="focus"
47
+ :lock="singleLock"
48
+ :locked="singleLocked"
41
49
  @submit="saveSingle"
42
50
  />
43
51
  </KCard>
52
+ </div>
44
53
 
45
- <template v-else-if="config.kind === 'page'">
46
- <div v-if="total || searchTerm" class="cms-toolbar">
47
- <KInput
48
- v-model="search"
49
- :icon="cmsIcon('magnifying-glass')"
50
- placeholder="Search…"
51
- aria-label="Search pages"
52
- class="cms-toolbar-search"
54
+ <KListPage
55
+ v-else
56
+ v-model:page="page"
57
+ v-model:page-size="pageSize"
58
+ v-model:search="search"
59
+ v-model:sort="sort"
60
+ v-model:selected="selected"
61
+ :title="config.label"
62
+ :icon="cmsIcon(kindMeta.icon)"
63
+ :description="kindMeta.description"
64
+ :rows="rows"
65
+ :columns="columns"
66
+ :row-key="rowKey"
67
+ :table-label="config.label"
68
+ :selectable="selectable"
69
+ :loading="pending"
70
+ :error="error ? errorMessage(error) : void 0"
71
+ error-title="Could not load entries"
72
+ :total="total"
73
+ :page-sizes="PAGE_SIZES"
74
+ :all-ids="selectable ? allIds : void 0"
75
+ fill
76
+ count
77
+ searchable
78
+ :search-placeholder="isPages ? 'Search pages' : 'Search entries'"
79
+ :search-label="isPages ? 'Search pages' : 'Search entries'"
80
+ toolbar-label="Search and filters"
81
+ :filtered="filterCount > 0"
82
+ :empty-icon="cmsIcon(isPages ? 'document-text' : 'sparkles')"
83
+ :empty-title="isPages ? 'No pages yet' : 'Nothing here yet'"
84
+ :empty-description="
85
+ isPages ? 'Pages come from the routes of the app.' : 'Entries you create will show up here.'
86
+ "
87
+ no-results-title="No matching entries"
88
+ no-results-description="Try another search or clear the filters."
89
+ clear-label="Clear filters"
90
+ :results-text="resultsLabel"
91
+ @retry="reload"
92
+ @clear="clearNarrowing"
93
+ @row-click="onSelect"
94
+ >
95
+ <template v-if="isCollectionKind(config)" #actions>
96
+ <KDropdownMenu v-if="transferItems.length" :items="transferItems" placement="bottom-end">
97
+ <KButton
98
+ :icon="cmsIcon('arrows-up-down')"
99
+ variant="ghost"
100
+ tone="neutral"
101
+ size="sm"
102
+ square
103
+ :loading="exporting"
104
+ aria-label="Import and export"
53
105
  />
54
- </div>
55
-
56
- <KCard v-if="pageTree.length" variant="outline" padding="none" class="cms-tree">
57
- <NuxtLink
58
- v-for="(node, index) in pageTree"
59
- :key="String(node.row.id)"
60
- :to="`/cms/${name}/${node.row.id}`"
61
- class="cms-tree-row"
62
- :style="{
63
- paddingLeft: `${1 + node.depth * 1.5}rem`,
64
- '--cms-row-delay': `${Math.min(index, 14) * 24}ms`
65
- }"
66
- >
67
- <KIcon
68
- v-if="node.depth"
69
- :name="cmsIcon('arrow-turn-left-up')"
70
- class="cms-tree-branch"
71
- />
72
- <span class="cms-tree-doc">
73
- <KIcon :name="cmsIcon('document-text')" />
74
- </span>
75
- <span class="cms-tree-text">
76
- <span class="cms-tree-title">{{ node.row.label }}</span>
77
- <span class="cms-tree-path">{{ node.row.path }}</span>
78
- </span>
79
- <span class="cms-tree-meta">{{ lastEdit(node.row.updatedAt) }}</span>
80
- <KIcon name="chevron-right" class="cms-tree-chevron" />
81
- </NuxtLink>
82
- </KCard>
83
-
84
- <KSpinner v-else-if="status === 'pending'" block />
85
-
86
- <KEmptyState
87
- v-else-if="searchTerm"
88
- :icon="cmsIcon('magnifying-glass')"
89
- title="No matching pages"
90
- />
91
-
92
- <KEmptyState
93
- v-else
94
- :icon="cmsIcon('document-text')"
95
- title="No pages yet"
96
- description="Pages come from the routes of the app."
97
- />
98
-
99
- <KPagination
100
- v-model:page="page"
101
- :total="total"
102
- :page-size="PAGE_SIZE"
103
- :show-summary="false"
104
- />
106
+ </KDropdownMenu>
107
+ <KCommandButton v-if="newCommand" :command="newCommand" size="sm" />
105
108
  </template>
106
109
 
107
- <template v-else>
108
- <div class="cms-toolbar">
109
- <KInput
110
- v-if="total || narrowed"
111
- v-model="search"
112
- :icon="cmsIcon('magnifying-glass')"
113
- placeholder="Search…"
114
- aria-label="Search entries"
115
- class="cms-toolbar-search"
116
- />
117
- <div class="cms-toolbar-actions">
118
- <KSelectionBar
119
- :count="selected.length"
120
- :total="rows.length"
121
- :on-select-all="selectAll"
122
- :on-clear="clearSelection"
123
- >
124
- <template #actions>
125
- <template v-if="drafts">
126
- <KButton
127
- label="Draft"
128
- :icon="cmsIcon('pencil-square')"
129
- variant="soft"
130
- :disabled="saving"
131
- @click="bulkStatus('draft')"
132
- />
133
- <KButton
134
- label="Publish"
135
- :icon="cmsIcon('check-circle')"
136
- variant="soft"
137
- :disabled="saving"
138
- @click="bulkStatus('published')"
139
- />
140
- </template>
141
- <KButton
142
- v-if="selected.length === 1"
143
- label="Duplicate"
144
- :icon="cmsIcon('document-duplicate')"
145
- variant="soft"
146
- :disabled="saving"
147
- @click="duplicateSelected"
148
- />
149
- <KButton
150
- label="Delete"
151
- :icon="cmsIcon('trash')"
152
- tone="danger"
153
- :loading="saving"
154
- @click="bulkDelete"
155
- />
156
- </template>
157
- </KSelectionBar>
158
- <template v-if="!selected.length">
159
- <KDropdownMenu :items="transferItems" placement="bottom-end">
160
- <KButton
161
- :icon="cmsIcon('arrows-up-down')"
162
- variant="soft"
163
- tone="neutral"
164
- :loading="exporting"
165
- aria-label="Import and export"
166
- title="Import and export"
167
- />
168
- </KDropdownMenu>
169
- <KButton label="New entry" icon="plus" @click="openCreate" />
170
- </template>
171
- </div>
172
- </div>
110
+ <template v-if="newCommand" #empty-actions>
111
+ <KCommandButton :command="newCommand" size="sm" />
112
+ </template>
173
113
 
114
+ <template v-if="filterKeys.length" #toolbar>
174
115
  <CmsListFilters
175
- v-if="filterKeys.length && (total || narrowed)"
176
116
  v-model="filters"
177
117
  :config="config"
178
118
  :keys="filterKeys"
179
119
  :relations="relations"
180
120
  />
121
+ </template>
181
122
 
182
- <KTable
183
- v-if="rows.length"
184
- v-model:selected="selected"
185
- v-model:sort="sort"
186
- selectable
187
- :rows="rows"
188
- :columns="columns"
189
- :row-key="rowKey"
190
- @row-click="onSelect"
191
- >
192
- <template v-for="key in mediaKeys" #[`cell-${key}`]="{ row }" :key="key">
193
- <CmsMediaThumb :value="mediaThumbValue(key, row[key])" />
194
- </template>
195
- <template v-if="drafts" #cell-status="{ row }">
196
- <CmsStatusBadge
197
- :published="row.status === 'published'"
198
- :scheduled="mounted && isContent && isScheduledContent(row)"
199
- :at="formatDateTime(row.publishedAt)"
200
- />
201
- </template>
202
- </KTable>
203
-
204
- <KSpinner v-else-if="status === 'pending'" block />
123
+ <template v-if="selectable" #bulk>
124
+ <template v-if="canBulkStatus">
125
+ <KButton
126
+ label="Draft"
127
+ :icon="cmsIcon('pencil-square')"
128
+ variant="soft"
129
+ tone="neutral"
130
+ size="sm"
131
+ :disabled="saving"
132
+ @click="bulkStatus('draft')"
133
+ />
134
+ <KButton
135
+ label="Publish"
136
+ :icon="cmsIcon('check-circle')"
137
+ variant="soft"
138
+ tone="neutral"
139
+ size="sm"
140
+ :disabled="saving"
141
+ @click="bulkStatus('published')"
142
+ />
143
+ </template>
144
+ <KButton
145
+ v-if="canCreate"
146
+ label="Duplicate"
147
+ :icon="cmsIcon('document-duplicate')"
148
+ variant="soft"
149
+ tone="neutral"
150
+ size="sm"
151
+ :disabled="saving || selected.length !== 1"
152
+ disabled-reason="Select one entry to duplicate"
153
+ @click="duplicateSelected"
154
+ />
155
+ <KButton
156
+ v-if="canDelete"
157
+ label="Delete"
158
+ icon="trash"
159
+ variant="soft"
160
+ tone="danger"
161
+ size="sm"
162
+ :loading="saving"
163
+ @click="bulkDelete"
164
+ />
165
+ </template>
205
166
 
206
- <KEmptyState
207
- v-else-if="narrowed"
208
- :icon="cmsIcon('magnifying-glass')"
209
- title="No matching entries"
210
- fill
167
+ <template v-if="isPages" #table>
168
+ <KSkeletonList
169
+ v-if="pending && !pageTree.length"
170
+ :rows="6"
171
+ :lines="2"
172
+ class="cms-list-loading"
173
+ />
174
+ <KTree
175
+ v-else-if="pageTree.length"
176
+ v-model:expanded="pageExpanded"
177
+ :nodes="pageTree"
178
+ :bordered="false"
179
+ :selectable="false"
180
+ label="Pages"
211
181
  >
212
- <template v-if="filterCount" #actions>
213
- <KButton label="Clear filters" icon="x-mark" variant="soft" @click="filters = {}" />
182
+ <template #meta="{ node }">
183
+ <KStack direction="row" gap="sm" align="center">
184
+ <CmsTranslationBadges
185
+ v-if="pageTranslations"
186
+ :progress="pageProgress(pageRows.get(String(node.id)))"
187
+ />
188
+ <span>{{ node.meta }}</span>
189
+ </KStack>
214
190
  </template>
215
- </KEmptyState>
216
-
191
+ </KTree>
217
192
  <KEmptyState
218
193
  v-else
219
- :icon="cmsIcon('sparkles')"
220
- title="Nothing here yet"
221
- description="Entries you create will show up here."
222
- fill
194
+ variant="plain"
195
+ size="sm"
196
+ icon="magnifying-glass"
197
+ title="No matching pages"
198
+ class="cms-list-loading"
223
199
  >
224
200
  <template #actions>
225
- <KButton label="New entry" icon="plus" variant="soft" @click="openCreate" />
201
+ <KButton
202
+ label="Clear search"
203
+ variant="soft"
204
+ tone="neutral"
205
+ size="sm"
206
+ @click="clearNarrowing"
207
+ />
226
208
  </template>
227
209
  </KEmptyState>
210
+ </template>
228
211
 
229
- <KPagination
230
- v-model:page="page"
231
- :total="total"
232
- :page-size="PAGE_SIZE"
233
- :show-summary="false"
212
+ <template v-for="key in mediaKeys" #[`cell-${key}`]="{ row }" :key="key">
213
+ <CmsMediaThumb :value="mediaThumbValue(key, row[key])" />
214
+ </template>
215
+ <template v-if="translationsShown" #[`cell-${TRANSLATIONS_COLUMN}`]="{ row }">
216
+ <CmsTranslationBadges :progress="rowProgress(row)" />
217
+ </template>
218
+ <template v-if="drafts" #cell-status="{ row }">
219
+ <CmsStatusBadge
220
+ :published="row.status === 'published'"
221
+ :scheduled="mounted && isContent && isScheduledContent(row)"
222
+ :at="formatDateTime(row.publishedAt)"
234
223
  />
224
+ </template>
235
225
 
226
+ <template v-if="isCollectionKind(config)">
236
227
  <CmsEntryDrawer
237
228
  v-if="!isContent"
238
229
  v-model:open="drawerOpen"
239
230
  :collection="name"
240
231
  :config="config"
241
- :entry-id="drawerEntryId"
242
- :initial="drawerInitial"
243
- @saved="refresh"
232
+ :entry-id="entryRecord.isNew.value ? null : entryRecord.id.value"
233
+ :duplicate-id="duplicateId"
234
+ :focus="drawerFocus"
235
+ @saved="() => refresh()"
236
+ @created="onCreated"
244
237
  @deleted="onDrawerDeleted"
245
- @duplicate="openDuplicate"
238
+ @duplicate="(id) => openRecord(CMS_NEW_RECORD, { duplicate: id })"
239
+ @missing="entryRecord.close()"
246
240
  />
247
241
 
248
242
  <CmsImportModal
@@ -252,7 +246,7 @@
252
246
  @imported="refresh"
253
247
  />
254
248
  </template>
255
- </div>
249
+ </KListPage>
256
250
  </template>
257
251
 
258
252
  <script setup>
@@ -261,8 +255,11 @@ import {
261
255
  isScheduledContent,
262
256
  isTranslatableField,
263
257
  isTranslatableMediaField,
258
+ linkDisplayText,
259
+ pageFields,
264
260
  pageParentPath,
265
261
  pickTranslatedMedia,
262
+ relationTarget,
266
263
  selectDisplayValue
267
264
  } from "#nuxt-cms";
268
265
  import {
@@ -271,6 +268,8 @@ import {
271
268
  definePageMeta,
272
269
  navigateTo,
273
270
  onMounted,
271
+ useRouter,
272
+ useState,
274
273
  ref,
275
274
  useFetch,
276
275
  useRoute,
@@ -279,41 +278,62 @@ import {
279
278
  import {
280
279
  Button as KButton,
281
280
  Card as KCard,
281
+ CommandButton as KCommandButton,
282
282
  DropdownMenu as KDropdownMenu,
283
283
  EmptyState as KEmptyState,
284
- Icon as KIcon,
285
- Input as KInput,
284
+ ListPage as KListPage,
286
285
  PageHeader as KPageHeader,
287
- Pagination as KPagination,
288
- SelectionBar as KSelectionBar,
289
- Spinner as KSpinner,
290
- Table as KTable
286
+ SkeletonList as KSkeletonList,
287
+ Stack as KStack,
288
+ Tree as KTree,
289
+ routeHashValue,
290
+ toast,
291
+ useLeaveGuard,
292
+ usePageCommands,
293
+ useRouteRecord
291
294
  } from "@xleddyl/443-components-vue";
292
295
  import cmsConfig from "#cms-config";
293
- import { duplicateEntryValues } from "../../shared/duplicate";
296
+ import { entryDefaults, lockedFieldKeys } from "../../shared/field-options";
294
297
  import {
298
+ LIST_FILTER_PREFIX,
299
+ LIST_SEARCH_PARAM as CMS_LIST_SEARCH_PARAM,
295
300
  entryListFilterKeys,
296
301
  listFilterQuery,
297
302
  parseListFilters,
298
303
  withoutListFilterQuery
299
304
  } from "../../shared/list-filters";
300
- import { readSearchFocus } from "../../shared/search";
305
+ import {
306
+ CMS_ENTRY_FOCUS_KEYS,
307
+ CMS_ENTRY_ROUTE_KEY,
308
+ CMS_ENTRY_TAB_ROUTE_KEY,
309
+ CMS_TAB_QUERY,
310
+ cmsPageKey,
311
+ readSearchFocus
312
+ } from "../../shared/search";
313
+ import {
314
+ TRANSLATIONS_COLUMN,
315
+ entryHasTranslations,
316
+ translationProgress
317
+ } from "../../shared/translations";
301
318
  import { canTransferCsv, exportFileName } from "../../shared/transfer";
302
- import { useCmsConfirm } from "../composables/cms-confirm";
319
+ import { useCmsPermissions } from "../composables/cms-permissions";
320
+ import { CMS_NEW_RECORD, useCmsPageQuery, useCmsRouteFlag } from "../composables/cms-route";
321
+ import { useCmsLinkedDelete } from "../composables/cms-link-usage";
303
322
  import { useCmsRuntime } from "../composables/cms-runtime";
304
323
  import { entryListColumns, useCmsSettingsState } from "../composables/cms-settings";
305
- import { useCmsToast } from "../composables/cms-toast";
306
324
  import { cmsApi } from "../utils/api";
307
- import { cmsIcon, errorMessage } from "../utils/ui";
325
+ import { cmsIcon, errorIssues, errorMessage } from "../utils/ui";
308
326
  definePageMeta({
309
327
  layout: "cms-admin",
310
328
  middleware: "cms-auth",
311
329
  validate: (route2) => Object.hasOwn(cmsConfig, route2.params.collection),
312
- key: (route2) => `${route2.path}${JSON.stringify(withoutListFilterQuery(route2.query))}`
330
+ key: (route2) => cmsPageKey({ path: route2.path, query: withoutListFilterQuery(route2.query) }, [
331
+ CMS_LIST_SEARCH_PARAM,
332
+ CMS_TAB_QUERY
333
+ ])
313
334
  });
314
335
  const FORM_ID = "cms-single-form";
315
336
  const route = useRoute();
316
- const toast = useCmsToast();
317
337
  const focus = readSearchFocus(route.query);
318
338
  const name = route.params.collection;
319
339
  const config = cmsConfig[name];
@@ -330,21 +350,51 @@ const kindMeta = { ...KIND_META[config.kind], icon: config.icon ?? KIND_META[con
330
350
  const fieldKeys = Object.keys(config.fields);
331
351
  const mediaKeys = fieldKeys.filter((key) => config.fields[key].type === "media");
332
352
  const isContent = config.kind === "content";
353
+ const isPages = config.kind === "page";
333
354
  const drafts = isCollectionKind(config) && !!config.drafts;
334
355
  const formKeys = drafts ? [...fieldKeys, "status"] : fieldKeys;
356
+ const { can, entryLock } = useCmsPermissions();
357
+ const canCreate = computed(() => can(`${name}.create`));
358
+ const canDelete = computed(() => can(`${name}.delete`));
359
+ const canBulkStatus = computed(() => drafts && can(`${name}.update`) && can(`${name}.publish`));
360
+ const selectable = computed(
361
+ () => isCollectionKind(config) && (canCreate.value || canDelete.value || canBulkStatus.value)
362
+ );
363
+ const singleLock = computed(
364
+ () => config.kind === "single" ? entryLock(name, config, { isNew: false }) : null
365
+ );
335
366
  const endpoint = `/api/cms/admin/${name}`;
336
- const PAGE_SIZE = 25;
367
+ const PAGE_SIZES = [25, 50, 100];
337
368
  const page = ref(1);
338
- const search = ref("");
339
- const searchTerm = ref("");
369
+ const pageSize = ref(25);
370
+ const limit = computed(() => typeof pageSize.value === "number" ? pageSize.value : 100);
371
+ const routeSearch = useCmsPageQuery(CMS_LIST_SEARCH_PARAM);
372
+ const search = ref(routeSearch.value);
373
+ const searchTerm = ref(search.value.trim());
340
374
  let searchTimer;
341
375
  watch(search, (value) => {
342
376
  clearTimeout(searchTimer);
343
377
  searchTimer = setTimeout(() => {
378
+ if (searchTerm.value === value.trim()) return;
344
379
  searchTerm.value = value.trim();
380
+ routeSearch.value = searchTerm.value;
345
381
  page.value = 1;
346
382
  }, 300);
347
383
  });
384
+ const router = useRouter();
385
+ const listPath = route.path;
386
+ function onListPage() {
387
+ return router.currentRoute.value.path === listPath;
388
+ }
389
+ watch(routeSearch, (value) => {
390
+ if (value === searchTerm.value || !onListPage()) return;
391
+ search.value = value;
392
+ searchTerm.value = value;
393
+ page.value = 1;
394
+ });
395
+ watch(pageSize, () => {
396
+ page.value = 1;
397
+ });
348
398
  const sort = ref(null);
349
399
  watch(sort, () => {
350
400
  page.value = 1;
@@ -353,26 +403,52 @@ const filters = computed({
353
403
  get: () => isCollectionKind(config) ? parseListFilters(config, route.query).filters : {},
354
404
  set: (value) => {
355
405
  void navigateTo(
356
- { query: { ...withoutListFilterQuery(route.query), ...listFilterQuery(value) } },
406
+ {
407
+ query: { ...withoutListFilterQuery(route.query), ...listFilterQuery(value) },
408
+ hash: route.hash
409
+ },
357
410
  { replace: true }
358
411
  );
359
412
  }
360
413
  });
361
- const filterQuery = computed(() => listFilterQuery(filters.value));
414
+ const filterQueryKey = computed(() => JSON.stringify(listFilterQuery(filters.value)));
415
+ const filterQuery = computed(() => JSON.parse(filterQueryKey.value));
362
416
  const filterCount = computed(() => Object.keys(filterQuery.value).length);
363
417
  const narrowed = computed(() => !!searchTerm.value || filterCount.value > 0);
364
- watch(
365
- () => JSON.stringify(filterQuery.value),
366
- () => {
367
- page.value = 1;
368
- }
369
- );
418
+ const rememberedQuery = useState(`cms-list-query-${name}`, () => ({}));
419
+ watch([filterQuery, searchTerm], () => {
420
+ if (!onListPage()) return;
421
+ rememberedQuery.value = {
422
+ ...filterQuery.value,
423
+ ...searchTerm.value ? { [CMS_LIST_SEARCH_PARAM]: searchTerm.value } : {}
424
+ };
425
+ });
426
+ onMounted(() => {
427
+ const remembered = rememberedQuery.value;
428
+ const hasListQuery = Object.keys(route.query).some(
429
+ (key) => key === CMS_LIST_SEARCH_PARAM || key.startsWith(LIST_FILTER_PREFIX)
430
+ );
431
+ if (hasListQuery || !Object.keys(remembered).length || !isCollectionKind(config)) return;
432
+ void navigateTo(
433
+ { query: { ...route.query, ...remembered }, hash: route.hash },
434
+ { replace: true }
435
+ );
436
+ });
437
+ function clearNarrowing() {
438
+ search.value = "";
439
+ searchTerm.value = "";
440
+ routeSearch.value = "";
441
+ filters.value = {};
442
+ }
443
+ watch(filterQueryKey, () => {
444
+ page.value = 1;
445
+ });
370
446
  const sortQuery = computed(
371
447
  () => sort.value ? { sort: sort.value.key, order: sort.value.direction } : {}
372
448
  );
373
449
  const listQuery = computed(() => ({
374
- limit: PAGE_SIZE,
375
- offset: (page.value - 1) * PAGE_SIZE,
450
+ limit: limit.value,
451
+ offset: (page.value - 1) * limit.value,
376
452
  ...searchTerm.value ? { search: searchTerm.value } : {},
377
453
  ...sortQuery.value,
378
454
  ...filterQuery.value
@@ -399,32 +475,76 @@ function lastEdit(value) {
399
475
  minute: "2-digit"
400
476
  })}`;
401
477
  }
478
+ const pageRows = computed(() => new Map(rows.value.map((row) => [String(row.id), row])));
402
479
  const pageTree = computed(() => {
403
- const byPath = new Set(rows.value.map((row) => String(row.path)));
404
- const depthOf = (path) => {
405
- let depth = 0;
480
+ const byPath = new Map(rows.value.map((row) => [String(row.path), row]));
481
+ const parentOf = (path) => {
406
482
  let parent = pageParentPath(path);
407
483
  while (parent) {
408
- if (byPath.has(parent)) depth++;
484
+ const row = byPath.get(parent);
485
+ if (row) return String(row.id);
409
486
  parent = pageParentPath(parent);
410
487
  }
411
- return depth;
488
+ return null;
412
489
  };
413
- return rows.value.map((row) => ({ row, depth: depthOf(String(row.path)) }));
490
+ const children = /* @__PURE__ */ new Map();
491
+ for (const row of rows.value) {
492
+ const parent = parentOf(String(row.path));
493
+ children.set(parent, [...children.get(parent) ?? [], row]);
494
+ }
495
+ const build = (parent) => (children.get(parent) ?? []).map((row) => {
496
+ const nested = build(String(row.id));
497
+ return {
498
+ id: String(row.id),
499
+ label: String(row.label),
500
+ description: String(row.path),
501
+ meta: lastEdit(row.updatedAt),
502
+ icon: cmsIcon("document-text"),
503
+ to: `/cms/${name}/${row.id}`,
504
+ children: nested.length ? nested : void 0
505
+ };
506
+ });
507
+ return build(null);
508
+ });
509
+ const collapsedPages = ref([]);
510
+ const pageExpanded = computed({
511
+ get: () => [...pageTreeParents.value].filter((id) => !collapsedPages.value.includes(id)),
512
+ set: (ids) => {
513
+ collapsedPages.value = [...pageTreeParents.value].filter((id) => !ids.includes(id));
514
+ }
515
+ });
516
+ const pageTreeParents = computed(() => {
517
+ const ids = /* @__PURE__ */ new Set();
518
+ const walk = (nodes) => {
519
+ for (const node of nodes) {
520
+ if (!node.children) continue;
521
+ ids.add(node.id);
522
+ walk(node.children);
523
+ }
524
+ };
525
+ walk(pageTree.value);
526
+ return ids;
414
527
  });
415
528
  const total = computed(() => isList(data.value) ? data.value.total : 0);
416
529
  const relations = computed(() => isList(data.value) ? data.value.relations ?? {} : {});
530
+ const pending = computed(() => status.value === "pending");
417
531
  const reload = async () => {
418
532
  await refresh();
419
- if (config.kind === "single" && !error.value) {
420
- formState.value = data.value && !isList(data.value) ? pickFields(data.value) : emptyState();
421
- }
533
+ if (config.kind === "single" && !error.value) resetSingle();
422
534
  };
423
535
  const saving = ref(false);
424
536
  const formState = ref({});
537
+ const { i18n: contentI18n } = useCmsRuntime();
425
538
  function emptyState() {
426
- return Object.fromEntries(formKeys.map((k) => [k, null]));
539
+ return {
540
+ ...Object.fromEntries(formKeys.map((k) => [k, null])),
541
+ ...entryDefaults(config.fields, contentI18n, { local: true })
542
+ };
427
543
  }
544
+ const singleForm = ref(null);
545
+ const singleLocked = computed(
546
+ () => config.kind === "single" && data.value && !isList(data.value) ? lockedFieldKeys(config.fields, data.value) : []
547
+ );
428
548
  function pickFields(row) {
429
549
  return Object.fromEntries(formKeys.map((k) => [k, row[k] ?? null]));
430
550
  }
@@ -436,24 +556,28 @@ async function submit(action) {
436
556
  await refresh();
437
557
  return true;
438
558
  } catch (error2) {
439
- toast.add({
440
- title: "Save failed",
441
- description: errorMessage(error2),
442
- color: "error"
443
- });
559
+ singleForm.value?.setIssues(errorIssues(error2));
560
+ toast.error("Save failed", { description: errorMessage(error2) });
444
561
  return false;
445
562
  } finally {
446
563
  saving.value = false;
447
564
  }
448
565
  }
449
- if (config.kind === "single") {
566
+ const singleSnapshot = ref("");
567
+ function resetSingle() {
450
568
  formState.value = data.value && !isList(data.value) ? pickFields(data.value) : emptyState();
569
+ singleSnapshot.value = JSON.stringify(formState.value);
451
570
  }
571
+ if (config.kind === "single") resetSingle();
572
+ useLeaveGuard(
573
+ () => config.kind === "single" && !singleLock.value && JSON.stringify(formState.value) !== singleSnapshot.value
574
+ );
452
575
  async function saveSingle() {
576
+ const submitted = JSON.stringify(formState.value);
453
577
  const ok = await submit(() => cmsApi(endpoint, { method: "PUT", body: formState.value }));
454
- if (ok) toast.add({ title: "Saved", color: "success" });
578
+ if (ok) singleSnapshot.value = submitted;
579
+ if (ok) toast.success("Saved");
455
580
  }
456
- const { i18n: contentI18n } = useCmsRuntime();
457
581
  function stripHtml(html) {
458
582
  return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
459
583
  }
@@ -482,7 +606,7 @@ function formatDateTime(value) {
482
606
  function relationLabel(field, key, id) {
483
607
  const title = relations.value[key]?.[String(id)];
484
608
  if (title == null || title === "") return `#${id}`;
485
- const target = cmsConfig[field.to];
609
+ const target = cmsConfig[relationTarget(field)];
486
610
  const titleField = target?.titleField ? target.fields[target.titleField] : void 0;
487
611
  const label = titleField && isTranslatableField(titleField) ? localized(title) : titleField?.type === "select" ? selectDisplayValue(titleField, title) : String(title);
488
612
  return label || `#${id}`;
@@ -500,6 +624,10 @@ function displayValue(field, key, value) {
500
624
  return truncate(stripHtml(String(value)));
501
625
  case "json":
502
626
  return truncate(JSON.stringify(value));
627
+ case "link":
628
+ return truncate(
629
+ linkDisplayText(value, contentI18n.defaultLocale, contentI18n.defaultLocale)
630
+ );
503
631
  case "datetime":
504
632
  return formatDateTime(value);
505
633
  case "select":
@@ -518,13 +646,22 @@ function displayValue(field, key, value) {
518
646
  }
519
647
  }
520
648
  function isSortable(field) {
521
- if (field.type === "blocks") return false;
649
+ if (field.type === "blocks" || field.type === "link") return false;
522
650
  return !(field.type === "relation" && field.cardinality === "many-to-many");
523
651
  }
524
652
  const settings = useCmsSettingsState();
653
+ const listColumns = computed(() => entryListColumns(name, settings.value, contentI18n));
654
+ const translationsShown = computed(() => listColumns.value.includes(TRANSLATIONS_COLUMN));
655
+ const pageTranslations = config.kind === "page" && contentI18n.locales.length > 1 && entryHasTranslations(config);
656
+ function rowProgress(row) {
657
+ return translationProgress(config.fields, row, contentI18n);
658
+ }
659
+ function pageProgress(row) {
660
+ return translationProgress(pageFields(config, String(row.path)), row, contentI18n);
661
+ }
525
662
  const columns = computed(
526
- () => entryListColumns(name, settings.value).map(
527
- (key) => key === "status" && !config.fields.status ? { key: "status", label: "Status", sortable: true } : {
663
+ () => listColumns.value.map(
664
+ (key) => key === TRANSLATIONS_COLUMN ? { key, label: "Translations" } : key === "status" && !config.fields.status ? { key: "status", label: "Status", sortable: true } : {
528
665
  key,
529
666
  label: config.fields[key].label,
530
667
  value: (row) => displayValue(config.fields[key], key, row[key]),
@@ -537,55 +674,68 @@ function rowKey(row) {
537
674
  }
538
675
  const filterKeys = computed(() => {
539
676
  if (!isCollectionKind(config)) return [];
540
- const shown = entryListFilterKeys(config, entryListColumns(name, settings.value));
677
+ const shown = entryListFilterKeys(config, listColumns.value);
541
678
  const active = Object.keys(filters.value).filter((key) => !shown.includes(key));
542
679
  return [...shown, ...active];
543
680
  });
544
- const drawerOpen = ref(false);
545
- const drawerEntryId = ref(null);
546
- const drawerInitial = ref(null);
681
+ const entryRecord = useRouteRecord(CMS_ENTRY_ROUTE_KEY, {
682
+ canCreate: () => canCreate.value,
683
+ linkedKeys: [CMS_ENTRY_TAB_ROUTE_KEY, "duplicate", ...CMS_ENTRY_FOCUS_KEYS]
684
+ });
685
+ const { openRecord } = entryRecord;
686
+ const drawerOpen = computed({
687
+ get: () => !isContent && !!entryRecord.param.value,
688
+ set: (value) => {
689
+ if (!value) void entryRecord.close();
690
+ }
691
+ });
692
+ const duplicateId = computed(
693
+ () => entryRecord.isNew.value ? routeHashValue(route.hash, "duplicate") ?? null : null
694
+ );
695
+ const drawerFocus = computed(
696
+ () => readSearchFocus(
697
+ Object.fromEntries(CMS_ENTRY_FOCUS_KEYS.map((key) => [key, routeHashValue(route.hash, key)]))
698
+ )
699
+ );
547
700
  function onSelect(row) {
548
701
  if (isContent) {
549
702
  void navigateTo(`/cms/${name}/${row.id}`);
550
703
  return;
551
704
  }
552
- drawerInitial.value = null;
553
- drawerEntryId.value = String(row.id);
554
- drawerOpen.value = true;
705
+ void openRecord(String(row.id));
555
706
  }
556
707
  function openCreate() {
557
708
  if (isContent) {
558
709
  void navigateTo(`/cms/${name}/new`);
559
710
  return;
560
711
  }
561
- drawerInitial.value = null;
562
- drawerEntryId.value = null;
563
- drawerOpen.value = true;
712
+ void openRecord(CMS_NEW_RECORD);
564
713
  }
565
- function openDuplicate(values) {
566
- drawerInitial.value = values;
567
- drawerEntryId.value = null;
568
- drawerOpen.value = true;
714
+ const newCommand = computed(
715
+ () => isCollectionKind(config) && canCreate.value ? {
716
+ id: "new-entry",
717
+ label: "New entry",
718
+ icon: "plus",
719
+ keywords: [config.label],
720
+ onSelect: openCreate
721
+ } : null
722
+ );
723
+ usePageCommands(() => newCommand.value ? [newCommand.value] : []);
724
+ function onCreated(id) {
725
+ void refresh();
726
+ void entryRecord.replaceId(id);
569
727
  }
570
- async function duplicateSelected() {
728
+ function duplicateSelected() {
571
729
  const id = selected.value.map(String)[0];
572
- if (!id || saving.value) return;
730
+ if (!id) return;
573
731
  if (isContent) {
574
732
  void navigateTo({ path: `/cms/${name}/new`, query: { duplicate: id } });
575
733
  return;
576
734
  }
577
- saving.value = true;
578
- try {
579
- const entry = await cmsApi(`${endpoint}/${id}`);
580
- selected.value = [];
581
- openDuplicate(duplicateEntryValues(config, entry));
582
- } catch (error2) {
583
- toast.add({ title: "Could not load entry", description: errorMessage(error2), color: "error" });
584
- } finally {
585
- saving.value = false;
586
- }
735
+ selected.value = [];
736
+ void openRecord(CMS_NEW_RECORD, { duplicate: id });
587
737
  }
588
- const importOpen = ref(false);
738
+ const importOpen = useCmsRouteFlag("import");
589
739
  const exporting = ref(false);
590
740
  const csvAvailable = canTransferCsv(config);
591
741
  async function exportEntries(format, narrow) {
@@ -606,7 +756,7 @@ async function exportEntries(format, narrow) {
606
756
  link.click();
607
757
  URL.revokeObjectURL(url);
608
758
  } catch (error2) {
609
- toast.add({ title: "Export failed", description: errorMessage(error2), color: "error" });
759
+ toast.error("Export failed", { description: errorMessage(error2) });
610
760
  } finally {
611
761
  exporting.value = false;
612
762
  }
@@ -618,39 +768,58 @@ const transferItems = computed(() => {
618
768
  onSelect: () => void exportEntries(format, narrow)
619
769
  }));
620
770
  return [
621
- ...narrowed.value ? exports(true) : [],
622
- ...exports(false),
623
- {
624
- label: "Import from file\u2026",
625
- onSelect: () => {
626
- importOpen.value = true;
771
+ ...can(`${name}.export`) ? [...narrowed.value ? exports(true) : [], ...exports(false)] : [],
772
+ ...can(`${name}.import`) && canCreate.value ? [
773
+ {
774
+ label: "Import from file\u2026",
775
+ onSelect: () => {
776
+ importOpen.value = true;
777
+ }
627
778
  }
628
- }
779
+ ] : []
629
780
  ];
630
781
  });
631
782
  async function onDrawerDeleted() {
783
+ const id = entryRecord.id.value;
784
+ if (id) selected.value = selected.value.filter((key) => String(key) !== id);
632
785
  if (rows.value.length === 1 && page.value > 1) page.value -= 1;
633
786
  else await refresh();
634
787
  }
635
- const confirmAction = useCmsConfirm();
788
+ const confirmLinkedDelete = useCmsLinkedDelete();
636
789
  const selected = ref([]);
637
- watch(rows, (current) => {
638
- const ids = new Set(current.map((row) => String(row.id)));
639
- selected.value = selected.value.filter((id) => ids.has(String(id)));
640
- });
641
- function selectAll() {
642
- selected.value = rows.value.map(rowKey);
643
- }
644
- function clearSelection() {
790
+ watch([searchTerm, filterQueryKey], () => {
645
791
  selected.value = [];
792
+ });
793
+ async function allIds() {
794
+ const { ids } = await cmsApi(endpoint, {
795
+ query: {
796
+ ids: true,
797
+ ...searchTerm.value ? { search: searchTerm.value } : {},
798
+ ...sortQuery.value,
799
+ ...filterQuery.value
800
+ }
801
+ });
802
+ return ids;
646
803
  }
647
804
  function entriesLabel(count) {
648
805
  return `${count} ${count === 1 ? "entry" : "entries"}`;
649
806
  }
807
+ function resultsLabel(count) {
808
+ return isPages ? `${count} ${count === 1 ? "page" : "pages"}` : entriesLabel(count);
809
+ }
810
+ const BULK_CONCURRENCY = 6;
811
+ async function settleInBatches(ids, action) {
812
+ const results = [];
813
+ for (let start = 0; start < ids.length; start += BULK_CONCURRENCY)
814
+ results.push(
815
+ ...await Promise.allSettled(ids.slice(start, start + BULK_CONCURRENCY).map(action))
816
+ );
817
+ return results;
818
+ }
650
819
  async function runBulk(ids, action) {
651
820
  saving.value = true;
652
821
  try {
653
- const results = await Promise.allSettled(ids.map(action));
822
+ const results = await settleInBatches(ids, action);
654
823
  const failed = results.flatMap(
655
824
  (result, index) => result.status === "rejected" ? [{ id: ids[index], reason: result.reason }] : []
656
825
  );
@@ -663,23 +832,30 @@ async function runBulk(ids, action) {
663
832
  }
664
833
  function reportBulk(done, total2, failed) {
665
834
  const succeeded = total2 - failed.length;
666
- if (succeeded) toast.add({ title: `${done}: ${entriesLabel(succeeded)}`, color: "success" });
835
+ if (succeeded) toast.success(`${done}: ${entriesLabel(succeeded)}`);
667
836
  if (failed.length) {
668
- toast.add({
669
- title: `${entriesLabel(failed.length)} not changed`,
670
- description: errorMessage(failed[0].reason),
671
- color: "error"
837
+ toast.error(`${entriesLabel(failed.length)} not changed`, {
838
+ description: errorMessage(failed[0].reason)
672
839
  });
673
840
  }
674
841
  }
675
842
  async function bulkDelete() {
676
843
  const ids = selected.value.map(String);
677
844
  if (!ids.length || saving.value) return;
678
- if (!await confirmAction(`Delete ${entriesLabel(ids.length)}?`)) return;
679
- const emptiesPage = ids.length >= rows.value.length && page.value > 1;
845
+ const titles = Object.fromEntries(
846
+ ids.map((id) => {
847
+ const row = rows.value.find((candidate) => String(candidate.id) === id);
848
+ const field = config?.titleField ? config.fields[config.titleField] : void 0;
849
+ const title = row && field && config?.titleField ? displayValue(field, config.titleField, row[config.titleField]) : "";
850
+ return [id, title || `#${id}`];
851
+ })
852
+ );
853
+ if (!await confirmLinkedDelete(name, ids, `Delete ${entriesLabel(ids.length)}?`, titles))
854
+ return;
680
855
  const failed = await runBulk(ids, (id) => cmsApi(`${endpoint}/${id}`, { method: "DELETE" }));
681
856
  reportBulk("Deleted", ids.length, failed);
682
- if (!failed.length && emptiesPage) page.value -= 1;
857
+ if (page.value > 1 && !rows.value.length)
858
+ page.value = Math.max(1, Math.ceil(total.value / limit.value));
683
859
  }
684
860
  async function bulkStatus(status2) {
685
861
  const ids = selected.value.map(String);