convex-cms 0.0.3 → 0.0.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/README.md +107 -60
  2. package/admin/README.md +99 -0
  3. package/admin/src/components/AdminLayout.tsx +22 -0
  4. package/admin/src/components/BreakingChangesWarningDialog.tsx +81 -0
  5. package/admin/src/components/BulkActionBar.tsx +190 -0
  6. package/admin/src/components/BulkOperationModal.tsx +177 -0
  7. package/admin/src/components/ContentEntryEditor.tsx +1104 -0
  8. package/admin/src/components/ContentTypeFormModal.tsx +1012 -0
  9. package/admin/src/components/ErrorBoundary.tsx +83 -0
  10. package/admin/src/components/ErrorState.tsx +147 -0
  11. package/admin/src/components/Header.tsx +294 -0
  12. package/admin/src/components/RouteGuard.tsx +264 -0
  13. package/admin/src/components/Sidebar.tsx +90 -0
  14. package/admin/src/components/TaxonomyEditor.tsx +348 -0
  15. package/admin/src/components/TermTree.tsx +533 -0
  16. package/admin/src/components/UploadDropzone.tsx +383 -0
  17. package/admin/src/components/VersionCompare.tsx +250 -0
  18. package/admin/src/components/VersionHistory.tsx +279 -0
  19. package/admin/src/components/VersionRollbackModal.tsx +79 -0
  20. package/admin/src/components/cmsds/CmsButton.tsx +101 -0
  21. package/admin/src/components/cmsds/CmsDialog.tsx +139 -0
  22. package/admin/src/components/cmsds/CmsDropdown.tsx +62 -0
  23. package/admin/src/components/cmsds/CmsEmptyState.tsx +54 -0
  24. package/admin/src/components/cmsds/CmsField.tsx +47 -0
  25. package/admin/src/components/cmsds/CmsPageHeader.tsx +35 -0
  26. package/admin/src/components/cmsds/CmsStatusBadge.tsx +153 -0
  27. package/admin/src/components/cmsds/CmsSurface.tsx +52 -0
  28. package/admin/src/components/cmsds/CmsTable.tsx +164 -0
  29. package/admin/src/components/cmsds/CmsToolbar.tsx +58 -0
  30. package/admin/src/components/cmsds/index.ts +10 -0
  31. package/admin/src/components/fields/BooleanField.tsx +74 -0
  32. package/admin/src/components/fields/CategoryField.tsx +394 -0
  33. package/admin/src/components/fields/DateField.tsx +173 -0
  34. package/admin/src/components/fields/DefaultFieldRenderer.tsx +74 -0
  35. package/admin/src/components/fields/FieldRenderer.tsx +180 -0
  36. package/admin/src/components/fields/FieldWrapper.tsx +57 -0
  37. package/admin/src/components/fields/JsonField.tsx +172 -0
  38. package/admin/src/components/fields/MediaField.tsx +367 -0
  39. package/admin/src/components/fields/MultiSelectField.tsx +118 -0
  40. package/admin/src/components/fields/NumberField.tsx +77 -0
  41. package/admin/src/components/fields/ReferenceField.tsx +386 -0
  42. package/admin/src/components/fields/RichTextField.tsx +171 -0
  43. package/admin/src/components/fields/SelectField.tsx +62 -0
  44. package/admin/src/components/fields/TagField.tsx +325 -0
  45. package/admin/src/components/fields/TextAreaField.tsx +68 -0
  46. package/admin/src/components/fields/TextField.tsx +56 -0
  47. package/admin/src/components/fields/index.ts +54 -0
  48. package/admin/src/components/fields/registry.ts +64 -0
  49. package/admin/src/components/fields/types.ts +217 -0
  50. package/admin/src/components/filters/TaxonomyFilter.tsx +254 -0
  51. package/admin/src/components/filters/index.ts +1 -0
  52. package/admin/src/components/index.ts +8 -0
  53. package/admin/src/components/media/MediaAssetActions.tsx +115 -0
  54. package/admin/src/components/media/MediaAssetEditDialog.tsx +217 -0
  55. package/admin/src/components/media/MediaBulkActionBar.tsx +51 -0
  56. package/admin/src/components/media/MediaFolderActions.tsx +69 -0
  57. package/admin/src/components/media/MediaFolderEditDialog.tsx +126 -0
  58. package/admin/src/components/media/MediaMoveModal.tsx +179 -0
  59. package/admin/src/components/media/MediaPreviewModal.tsx +370 -0
  60. package/admin/src/components/media/MediaTaxonomyPicker.tsx +304 -0
  61. package/admin/src/components/media/MediaTrashBulkActionBar.tsx +59 -0
  62. package/admin/src/components/ui/accordion.tsx +64 -0
  63. package/admin/src/components/ui/alert-dialog.tsx +155 -0
  64. package/admin/src/components/ui/alert.tsx +66 -0
  65. package/admin/src/components/ui/avatar.tsx +53 -0
  66. package/admin/src/components/ui/badge.tsx +46 -0
  67. package/admin/src/components/ui/breadcrumb.tsx +109 -0
  68. package/admin/src/components/ui/button.tsx +62 -0
  69. package/admin/src/components/ui/calendar.tsx +220 -0
  70. package/admin/src/components/ui/card.tsx +92 -0
  71. package/admin/src/components/ui/checkbox.tsx +30 -0
  72. package/admin/src/components/ui/command.tsx +182 -0
  73. package/admin/src/components/ui/dialog.tsx +143 -0
  74. package/admin/src/components/ui/dropdown-menu.tsx +257 -0
  75. package/admin/src/components/ui/form.tsx +167 -0
  76. package/admin/src/components/ui/input.tsx +21 -0
  77. package/admin/src/components/ui/label.tsx +24 -0
  78. package/admin/src/components/ui/popover.tsx +46 -0
  79. package/admin/src/components/ui/scroll-area.tsx +56 -0
  80. package/admin/src/components/ui/select.tsx +190 -0
  81. package/admin/src/components/ui/separator.tsx +26 -0
  82. package/admin/src/components/ui/sheet.tsx +137 -0
  83. package/admin/src/components/ui/sidebar.tsx +724 -0
  84. package/admin/src/components/ui/skeleton.tsx +13 -0
  85. package/admin/src/components/ui/sonner.tsx +38 -0
  86. package/admin/src/components/ui/switch.tsx +31 -0
  87. package/admin/src/components/ui/table.tsx +114 -0
  88. package/admin/src/components/ui/tabs.tsx +66 -0
  89. package/admin/src/components/ui/textarea.tsx +18 -0
  90. package/admin/src/components/ui/tooltip.tsx +61 -0
  91. package/admin/src/contexts/AdminConfigContext.tsx +30 -0
  92. package/admin/src/contexts/AuthContext.tsx +330 -0
  93. package/admin/src/contexts/BreadcrumbContext.tsx +49 -0
  94. package/admin/src/contexts/SettingsConfigContext.tsx +57 -0
  95. package/admin/src/contexts/ThemeContext.tsx +91 -0
  96. package/admin/src/contexts/index.ts +20 -0
  97. package/admin/src/embed/components/EmbedHeader.tsx +103 -0
  98. package/admin/src/embed/components/EmbedLayout.tsx +29 -0
  99. package/admin/src/embed/components/EmbedSidebar.tsx +119 -0
  100. package/admin/src/embed/components/index.ts +3 -0
  101. package/admin/src/embed/contexts/ApiContext.tsx +32 -0
  102. package/admin/src/embed/index.tsx +184 -0
  103. package/admin/src/embed/navigation.tsx +202 -0
  104. package/admin/src/embed/pages/Content.tsx +19 -0
  105. package/admin/src/embed/pages/ContentTypes.tsx +19 -0
  106. package/admin/src/embed/pages/Dashboard.tsx +19 -0
  107. package/admin/src/embed/pages/Media.tsx +19 -0
  108. package/admin/src/embed/pages/Settings.tsx +22 -0
  109. package/admin/src/embed/pages/Taxonomies.tsx +22 -0
  110. package/admin/src/embed/pages/Trash.tsx +22 -0
  111. package/admin/src/embed/pages/index.ts +7 -0
  112. package/admin/src/embed/types.ts +24 -0
  113. package/admin/src/hooks/index.ts +2 -0
  114. package/admin/src/hooks/use-mobile.ts +19 -0
  115. package/admin/src/hooks/useBreadcrumbLabel.ts +15 -0
  116. package/admin/src/hooks/usePermissions.ts +211 -0
  117. package/admin/src/lib/admin-config.ts +111 -0
  118. package/admin/src/lib/cn.ts +6 -0
  119. package/admin/src/lib/config.server.ts +56 -0
  120. package/admin/src/lib/convex.ts +26 -0
  121. package/admin/src/lib/embed-adapter.ts +80 -0
  122. package/admin/src/lib/icons.tsx +96 -0
  123. package/admin/src/lib/loadAdminConfig.ts +92 -0
  124. package/admin/src/lib/motion.ts +29 -0
  125. package/admin/src/lib/navigation.ts +43 -0
  126. package/admin/src/lib/tanstack-adapter.ts +82 -0
  127. package/admin/src/pages/ContentPage.tsx +337 -0
  128. package/admin/src/pages/ContentTypesPage.tsx +457 -0
  129. package/admin/src/pages/DashboardPage.tsx +163 -0
  130. package/admin/src/pages/MediaPage.tsx +34 -0
  131. package/admin/src/pages/SettingsPage.tsx +486 -0
  132. package/admin/src/pages/TaxonomiesPage.tsx +289 -0
  133. package/admin/src/pages/TrashPage.tsx +421 -0
  134. package/admin/src/pages/index.ts +14 -0
  135. package/admin/src/routeTree.gen.ts +262 -0
  136. package/admin/src/router.tsx +22 -0
  137. package/admin/src/routes/__root.tsx +250 -0
  138. package/admin/src/routes/content-types.tsx +20 -0
  139. package/admin/src/routes/content.tsx +20 -0
  140. package/admin/src/routes/entries/$entryId.tsx +107 -0
  141. package/admin/src/routes/entries/new.$contentTypeId.tsx +69 -0
  142. package/admin/src/routes/entries/type/$contentTypeId.tsx +503 -0
  143. package/admin/src/routes/index.tsx +20 -0
  144. package/admin/src/routes/media.tsx +1095 -0
  145. package/admin/src/routes/settings.tsx +20 -0
  146. package/admin/src/routes/taxonomies.tsx +20 -0
  147. package/admin/src/routes/trash.tsx +20 -0
  148. package/admin/src/styles/globals.css +69 -0
  149. package/admin/src/styles/tailwind-config.css +74 -0
  150. package/admin/src/styles/theme.css +73 -0
  151. package/admin/src/types/index.ts +221 -0
  152. package/admin/src/utils/errorParsing.ts +163 -0
  153. package/admin/src/utils/index.ts +5 -0
  154. package/admin/src/vite-env.d.ts +14 -0
  155. package/admin/tailwind.preset.cjs +102 -0
  156. package/admin-dist/nitro.json +1 -1
  157. package/admin-dist/public/assets/{CmsEmptyState-CRswfTzk.js → CmsEmptyState-CkqBIab3.js} +2 -2
  158. package/admin-dist/public/assets/{CmsPageHeader-CirpXndm.js → CmsPageHeader-CUtl5MMG.js} +1 -1
  159. package/admin-dist/public/assets/{CmsStatusBadge-CbEUpQu-.js → CmsStatusBadge-CUYFgEe-.js} +1 -1
  160. package/admin-dist/public/assets/CmsSurface-CsJfAVa3.js +1 -0
  161. package/admin-dist/public/assets/{CmsToolbar-BI2nZOXp.js → CmsToolbar-CnfbcxeP.js} +1 -1
  162. package/admin-dist/public/assets/{ContentEntryEditor-CBeCyK_m.js → ContentEntryEditor-BU220CCy.js} +1 -1
  163. package/admin-dist/public/assets/TaxonomyFilter-CWCxC5HZ.js +1 -0
  164. package/admin-dist/public/assets/_contentTypeId-DK8cskRt.js +1 -0
  165. package/admin-dist/public/assets/{_entryId-CKU_glsK.js → _entryId-CuVMExbb.js} +1 -1
  166. package/admin-dist/public/assets/alert-CF1BSzGR.js +1 -0
  167. package/admin-dist/public/assets/{badge-hvUOzpVZ.js → badge-CmuOIVKp.js} +1 -1
  168. package/admin-dist/public/assets/{circle-check-big-CF_pR17r.js → circle-check-big-BKDVG6DU.js} +1 -1
  169. package/admin-dist/public/assets/{command-DU82cJlt.js → command-XJxnF2Sd.js} +1 -1
  170. package/admin-dist/public/assets/content-QBUxdxbS.js +1 -0
  171. package/admin-dist/public/assets/content-types-CrNEm8Hf.js +2 -0
  172. package/admin-dist/public/assets/globals-B7Wsfh_v.css +1 -0
  173. package/admin-dist/public/assets/index-C7xOwudI.js +1 -0
  174. package/admin-dist/public/assets/{label-KNtpL71g.js → label-CHCnXeBk.js} +1 -1
  175. package/admin-dist/public/assets/{link-2-Bw2aI4V4.js → link-2-Bb34judH.js} +1 -1
  176. package/admin-dist/public/assets/{list-sYepHjt_.js → list-9Pzt48ld.js} +1 -1
  177. package/admin-dist/public/assets/{main-CKj5yfEi.js → main-CjQ2VI9L.js} +3 -3
  178. package/admin-dist/public/assets/media-Dc5PWt2Q.js +1 -0
  179. package/admin-dist/public/assets/{new._contentTypeId-C3LstjNs.js → new._contentTypeId-C_I4YxIa.js} +1 -1
  180. package/admin-dist/public/assets/{plus-DUn8v_Xf.js → plus-Ceef7DHk.js} +1 -1
  181. package/admin-dist/public/assets/{rotate-ccw-DJEoHcRI.js → rotate-ccw-7k7-4VUq.js} +1 -1
  182. package/admin-dist/public/assets/scroll-area-CC6wujnp.js +1 -0
  183. package/admin-dist/public/assets/{search-MuAUDJKR.js → search-DwoUV2pv.js} +1 -1
  184. package/admin-dist/public/assets/select-hOZTp8aC.js +1 -0
  185. package/admin-dist/public/assets/settings-t2PbCZh4.js +1 -0
  186. package/admin-dist/public/assets/switch-jX2pDaNU.js +1 -0
  187. package/admin-dist/public/assets/tabs-q4EbZk7c.js +1 -0
  188. package/admin-dist/public/assets/tanstack-adapter-B-Glm4kH.js +1 -0
  189. package/admin-dist/public/assets/taxonomies-kyk5P4ZW.js +1 -0
  190. package/admin-dist/public/assets/{textarea-BTy7nwzR.js → textarea-B6SfBmr0.js} +1 -1
  191. package/admin-dist/public/assets/trash-BOCnIznD.js +1 -0
  192. package/admin-dist/public/assets/{triangle-alert-E52Vfeuh.js → triangle-alert-CXFIO_Gu.js} +1 -1
  193. package/admin-dist/public/assets/useBreadcrumbLabel-_6qBagc3.js +1 -0
  194. package/admin-dist/public/assets/{usePermissions-Basjs9BT.js → usePermissions-M1ijZ7a6.js} +1 -1
  195. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +7 -0
  196. package/admin-dist/server/_ssr/{badge-6BsP37vG.mjs → CmsButton-DOiTVKQq.mjs} +33 -33
  197. package/admin-dist/server/_ssr/{CmsEmptyState-DU7-7-mV.mjs → CmsEmptyState-fbnGt3LD.mjs} +2 -2
  198. package/admin-dist/server/_ssr/{CmsPageHeader-CseW0AHm.mjs → CmsPageHeader-DHRrdOZa.mjs} +1 -1
  199. package/admin-dist/server/_ssr/{CmsStatusBadge-B_pi4KCp.mjs → CmsStatusBadge-s7obWbKZ.mjs} +2 -2
  200. package/admin-dist/server/_ssr/CmsSurface-rFoYjb62.mjs +44 -0
  201. package/admin-dist/server/_ssr/{CmsToolbar-X75ex6ek.mjs → CmsToolbar-zTE45z2q.mjs} +2 -2
  202. package/admin-dist/server/_ssr/{ContentEntryEditor-CepusRsA.mjs → ContentEntryEditor-BLoEjT_m.mjs} +12 -12
  203. package/admin-dist/server/_ssr/{TaxonomyFilter-Bwrq0-cz.mjs → TaxonomyFilter-XAtaJC2z.mjs} +5 -5
  204. package/admin-dist/server/_ssr/{_contentTypeId-BqYKEcLr.mjs → _contentTypeId-Csl4822C.mjs} +13 -13
  205. package/admin-dist/server/_ssr/{_entryId-CRfnqeDf.mjs → _entryId-D8alLFBx.mjs} +15 -15
  206. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BffZedId.mjs +4 -0
  207. package/admin-dist/server/_ssr/{command-fy8epIKf.mjs → command-C0Di14--.mjs} +1 -1
  208. package/admin-dist/server/_ssr/{content-B5RhL7uW.mjs → content-CT-FPsmV.mjs} +170 -98
  209. package/admin-dist/server/_ssr/{content-types-BIOqCQYN.mjs → content-types-C8cBFdzE.mjs} +260 -115
  210. package/admin-dist/server/_ssr/{index-DHSHDPt1.mjs → index-BJtcrEc-.mjs} +88 -17
  211. package/admin-dist/server/_ssr/index.mjs +2 -2
  212. package/admin-dist/server/_ssr/{label-C8Dko1j7.mjs → label-qn2Afwl4.mjs} +1 -1
  213. package/admin-dist/server/_ssr/{media-CSx3XttC.mjs → media-qv5IAsMZ.mjs} +43 -43
  214. package/admin-dist/server/_ssr/{new._contentTypeId-DzanEZQM.mjs → new._contentTypeId-DdGyrhqs.mjs} +13 -13
  215. package/admin-dist/server/_ssr/{router-DDWcF-kt.mjs → router-nSVkxb6Y.mjs} +11 -11
  216. package/admin-dist/server/_ssr/{scroll-area-bjPYwhXN.mjs → scroll-area-BCinP455.mjs} +1 -1
  217. package/admin-dist/server/_ssr/{select-BUhDDf4T.mjs → select-BKQlQScw.mjs} +1 -1
  218. package/admin-dist/server/_ssr/{settings-DAsxnw2q.mjs → settings-BCr2KQlk.mjs} +236 -139
  219. package/admin-dist/server/_ssr/{switch-BgyRtQ1Z.mjs → switch-BaOi42fE.mjs} +1 -1
  220. package/admin-dist/server/_ssr/{tabs-DzMdRB1A.mjs → tabs-DYXEi9kq.mjs} +5 -3
  221. package/admin-dist/server/_ssr/tanstack-adapter-Bsz8kha-.mjs +119 -0
  222. package/admin-dist/server/_ssr/{taxonomies-C8j8g5Q5.mjs → taxonomies-CueMHTbE.mjs} +184 -73
  223. package/admin-dist/server/_ssr/{textarea-9jNeYJSc.mjs → textarea-CI0Jqx2x.mjs} +1 -1
  224. package/admin-dist/server/_ssr/{trash-DYMxwhZB.mjs → trash-DE6W8GoX.mjs} +211 -88
  225. package/admin-dist/server/_ssr/{useBreadcrumbLabel-FNSAr2Ha.mjs → useBreadcrumbLabel-B5Yi72lM.mjs} +1 -1
  226. package/admin-dist/server/_ssr/{usePermissions-BJGGahrJ.mjs → usePermissions-C3nZ-Izm.mjs} +1 -1
  227. package/admin-dist/server/index.mjs +189 -182
  228. package/dist/cli/commands/init.d.ts +6 -0
  229. package/dist/cli/commands/init.d.ts.map +1 -0
  230. package/dist/cli/commands/init.js +156 -0
  231. package/dist/cli/commands/init.js.map +1 -0
  232. package/dist/cli/index.js +6 -0
  233. package/dist/cli/index.js.map +1 -1
  234. package/dist/client/admin/bulk.d.ts +79 -0
  235. package/dist/client/admin/bulk.d.ts.map +1 -0
  236. package/dist/client/admin/bulk.js +72 -0
  237. package/dist/client/admin/bulk.js.map +1 -0
  238. package/dist/client/admin/contentLock.d.ts +118 -0
  239. package/dist/client/admin/contentLock.d.ts.map +1 -0
  240. package/dist/client/admin/contentLock.js +81 -0
  241. package/dist/client/admin/contentLock.js.map +1 -0
  242. package/dist/client/admin/contentTypes.d.ts +1204 -0
  243. package/dist/client/admin/contentTypes.d.ts.map +1 -0
  244. package/dist/client/admin/contentTypes.js +122 -0
  245. package/dist/client/admin/contentTypes.js.map +1 -0
  246. package/dist/client/admin/dashboard.d.ts +16 -0
  247. package/dist/client/admin/dashboard.d.ts.map +1 -0
  248. package/dist/client/admin/dashboard.js +33 -0
  249. package/dist/client/admin/dashboard.js.map +1 -0
  250. package/dist/client/admin/entries.d.ts +358 -0
  251. package/dist/client/admin/entries.d.ts.map +1 -0
  252. package/dist/client/admin/entries.js +220 -0
  253. package/dist/client/admin/entries.js.map +1 -0
  254. package/dist/client/admin/index.d.ts +6568 -0
  255. package/dist/client/admin/index.d.ts.map +1 -0
  256. package/dist/client/admin/index.js +305 -0
  257. package/dist/client/admin/index.js.map +1 -0
  258. package/dist/client/admin/media.d.ts +1038 -0
  259. package/dist/client/admin/media.d.ts.map +1 -0
  260. package/dist/client/admin/media.js +489 -0
  261. package/dist/client/admin/media.js.map +1 -0
  262. package/dist/client/admin/taxonomies.d.ts +339 -0
  263. package/dist/client/admin/taxonomies.d.ts.map +1 -0
  264. package/dist/client/admin/taxonomies.js +364 -0
  265. package/dist/client/admin/taxonomies.js.map +1 -0
  266. package/dist/client/admin/trash.d.ts +91 -0
  267. package/dist/client/admin/trash.d.ts.map +1 -0
  268. package/dist/client/admin/trash.js +71 -0
  269. package/dist/client/admin/trash.js.map +1 -0
  270. package/dist/client/admin/types.d.ts +320 -0
  271. package/dist/client/admin/types.d.ts.map +1 -0
  272. package/dist/client/admin/types.js +7 -0
  273. package/dist/client/admin/types.js.map +1 -0
  274. package/dist/client/admin/validators.d.ts +3886 -0
  275. package/dist/client/admin/validators.d.ts.map +1 -0
  276. package/dist/client/admin/validators.js +322 -0
  277. package/dist/client/admin/validators.js.map +1 -0
  278. package/dist/client/admin/versions.d.ts +106 -0
  279. package/dist/client/admin/versions.d.ts.map +1 -0
  280. package/dist/client/admin/versions.js +57 -0
  281. package/dist/client/admin/versions.js.map +1 -0
  282. package/dist/client/adminApiTypes.d.ts +27 -0
  283. package/dist/client/adminApiTypes.d.ts.map +1 -0
  284. package/dist/client/adminApiTypes.js +12 -0
  285. package/dist/client/adminApiTypes.js.map +1 -0
  286. package/dist/client/{admin-config.d.ts → adminConfig.d.ts} +4 -4
  287. package/dist/client/adminConfig.d.ts.map +1 -0
  288. package/dist/client/{admin-config.js → adminConfig.js} +3 -3
  289. package/dist/client/adminConfig.js.map +1 -0
  290. package/dist/client/agentTools.d.ts +11 -21
  291. package/dist/client/agentTools.d.ts.map +1 -1
  292. package/dist/client/agentTools.js +4 -4
  293. package/dist/client/index.d.ts +6 -6
  294. package/dist/client/index.d.ts.map +1 -1
  295. package/dist/client/index.js +19 -6
  296. package/dist/client/index.js.map +1 -1
  297. package/dist/client/schema/codegen.d.ts +2 -2
  298. package/dist/client/schema/codegen.d.ts.map +1 -1
  299. package/dist/client/schema/codegen.js +3 -3
  300. package/dist/client/schema/codegen.js.map +1 -1
  301. package/dist/client/schema/defineContentType.d.ts +3 -3
  302. package/dist/client/schema/defineContentType.js +3 -3
  303. package/dist/client/schema/index.d.ts +7 -7
  304. package/dist/client/schema/index.d.ts.map +1 -1
  305. package/dist/client/schema/index.js +5 -5
  306. package/dist/client/schema/index.js.map +1 -1
  307. package/dist/client/schema/schemaDrift.d.ts +1 -1
  308. package/dist/client/schema/schemaDrift.js +1 -1
  309. package/dist/client/schema/typedClient.d.ts +2 -2
  310. package/dist/client/schema/typedClient.js +2 -2
  311. package/dist/client/schema/types.d.ts +1 -1
  312. package/dist/client/schema/types.js +1 -1
  313. package/dist/client/wrapper.d.ts +108 -65
  314. package/dist/client/wrapper.d.ts.map +1 -1
  315. package/dist/client/wrapper.js +22 -22
  316. package/dist/client/wrapper.js.map +1 -1
  317. package/dist/component/contentEntries.d.ts +4 -4
  318. package/dist/component/contentEntryMutations.d.ts +46 -0
  319. package/dist/component/contentEntryMutations.d.ts.map +1 -1
  320. package/dist/component/contentEntryMutations.js +1 -1
  321. package/dist/component/contentEntryMutations.js.map +1 -1
  322. package/dist/component/contentTypeMigration.d.ts +1 -1
  323. package/dist/component/contentTypeMutations.d.ts +22 -0
  324. package/dist/component/contentTypeMutations.d.ts.map +1 -1
  325. package/dist/component/contentTypeMutations.js +1 -1
  326. package/dist/component/contentTypeMutations.js.map +1 -1
  327. package/dist/component/convex.config.d.ts +2 -2
  328. package/dist/component/convex.config.js +2 -2
  329. package/dist/component/index.d.ts +1 -1
  330. package/dist/component/index.js +1 -1
  331. package/dist/component/lib/ragContentChunker.d.ts +1 -1
  332. package/dist/component/lib/ragContentChunker.js +1 -1
  333. package/dist/component/mediaAssetMutations.d.ts +47 -0
  334. package/dist/component/mediaAssetMutations.d.ts.map +1 -1
  335. package/dist/component/mediaAssetMutations.js +1 -1
  336. package/dist/component/mediaAssetMutations.js.map +1 -1
  337. package/dist/component/roles.d.ts +1 -1
  338. package/dist/component/roles.js +1 -1
  339. package/dist/component/schema.d.ts +9 -0
  340. package/dist/component/schema.d.ts.map +1 -1
  341. package/dist/component/schema.js +1 -1
  342. package/dist/component/schema.js.map +1 -1
  343. package/dist/react/index.d.ts +2 -2
  344. package/dist/react/index.d.ts.map +1 -1
  345. package/dist/react/index.js +13 -7
  346. package/dist/react/index.js.map +1 -1
  347. package/dist/test.d.ts +2 -2
  348. package/dist/test.js +2 -2
  349. package/package.json +115 -13
  350. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +0 -1
  351. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +0 -1
  352. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +0 -1
  353. package/admin-dist/public/assets/alert-BXjTqrwQ.js +0 -1
  354. package/admin-dist/public/assets/content-_LXl3pp7.js +0 -1
  355. package/admin-dist/public/assets/content-types-KjxaXGxY.js +0 -2
  356. package/admin-dist/public/assets/globals-CS6BZ0zp.css +0 -1
  357. package/admin-dist/public/assets/index-DNGIZHL-.js +0 -1
  358. package/admin-dist/public/assets/media-Bkrkffm7.js +0 -1
  359. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +0 -1
  360. package/admin-dist/public/assets/select-BD29IXCI.js +0 -1
  361. package/admin-dist/public/assets/settings-DmMyn_6A.js +0 -1
  362. package/admin-dist/public/assets/switch-h3Rrnl5i.js +0 -1
  363. package/admin-dist/public/assets/tabs-imc8h-Dp.js +0 -1
  364. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +0 -1
  365. package/admin-dist/public/assets/trash-SAWKZZHv.js +0 -1
  366. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +0 -1
  367. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +0 -89
  368. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +0 -4
  369. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +0 -92
  370. package/dist/client/admin-config.d.ts.map +0 -1
  371. package/dist/client/admin-config.js.map +0 -1
  372. package/dist/client/adminApi.d.ts +0 -2273
  373. package/dist/client/adminApi.d.ts.map +0 -1
  374. package/dist/client/adminApi.js +0 -716
  375. package/dist/client/adminApi.js.map +0 -1
@@ -0,0 +1,1104 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react'
2
+ import { useMutation } from 'convex/react'
3
+ import { api } from '../../convex/_generated/api'
4
+ import { FieldRenderer } from './fields/FieldRenderer'
5
+ import { VersionHistory } from './VersionHistory'
6
+ import type { FieldDefinition, FieldError } from './fields/types'
7
+ import { parseServerError, isRetryableError } from '~/utils'
8
+ import { useSettingsConfig } from '~/contexts'
9
+ import { CmsButton } from '~/components/cmsds/CmsButton'
10
+ import { CmsStatusBadge } from '~/components/cmsds/CmsStatusBadge'
11
+ import { CmsDialog, CmsConfirmDialog } from '~/components/cmsds/CmsDialog'
12
+ import { Badge as _Badge } from '~/components/ui/badge'
13
+ import { Input } from '~/components/ui/input'
14
+ import {
15
+ CheckCircle,
16
+ AlertCircle,
17
+ Loader2,
18
+ RefreshCw,
19
+ History,
20
+ Clock,
21
+ } from 'lucide-react'
22
+ import { cn } from '~/lib/cn'
23
+
24
+ function formatDateTimeLocal(timestamp: number): string {
25
+ const date = new Date(timestamp)
26
+ const year = date.getFullYear()
27
+ const month = String(date.getMonth() + 1).padStart(2, '0')
28
+ const day = String(date.getDate()).padStart(2, '0')
29
+ const hours = String(date.getHours()).padStart(2, '0')
30
+ const minutes = String(date.getMinutes()).padStart(2, '0')
31
+ return `${year}-${month}-${day}T${hours}:${minutes}`
32
+ }
33
+
34
+ function parseDateTimeLocal(value: string): number {
35
+ return new Date(value).getTime()
36
+ }
37
+
38
+ function formatDateOnly(timestamp: number): string {
39
+ const date = new Date(timestamp)
40
+ const year = date.getFullYear()
41
+ const month = String(date.getMonth() + 1).padStart(2, '0')
42
+ const day = String(date.getDate()).padStart(2, '0')
43
+ return `${year}-${month}-${day}`
44
+ }
45
+
46
+ function transformDataForUI(
47
+ data: Record<string, unknown>,
48
+ fields: FieldDefinition[]
49
+ ): Record<string, unknown> {
50
+ const transformed = { ...data }
51
+ for (const field of fields) {
52
+ const value = data[field.name]
53
+ if (
54
+ (field.type === 'date' || field.type === 'datetime') &&
55
+ typeof value === 'number'
56
+ ) {
57
+ transformed[field.name] =
58
+ field.type === 'datetime'
59
+ ? formatDateTimeLocal(value)
60
+ : formatDateOnly(value)
61
+ }
62
+ }
63
+ return transformed
64
+ }
65
+
66
+ function transformDataForBackend(
67
+ data: Record<string, unknown>,
68
+ fields: FieldDefinition[]
69
+ ): Record<string, unknown> {
70
+ const transformed = { ...data }
71
+ for (const field of fields) {
72
+ const value = data[field.name]
73
+ if (field.type === 'date' || field.type === 'datetime') {
74
+ if (typeof value === 'string' && value) {
75
+ transformed[field.name] = new Date(value).getTime()
76
+ } else if (!value) {
77
+ transformed[field.name] = null
78
+ }
79
+ }
80
+ }
81
+ return transformed
82
+ }
83
+
84
+ export interface ContentType {
85
+ _id: string
86
+ name: string
87
+ displayName: string
88
+ description?: string
89
+ fields: FieldDefinition[]
90
+ titleField?: string
91
+ slugField?: string
92
+ singleton?: boolean
93
+ isActive: boolean
94
+ }
95
+
96
+ export interface ContentEntry {
97
+ _id: string
98
+ contentTypeId: string
99
+ slug: string
100
+ status: 'draft' | 'published' | 'scheduled' | 'archived'
101
+ data: Record<string, unknown>
102
+ version: number
103
+ scheduledPublishAt?: number
104
+ firstPublishedAt?: number
105
+ lastPublishedAt?: number
106
+ }
107
+
108
+ interface ContentEntryEditorProps {
109
+ contentType: ContentType
110
+ entry?: ContentEntry
111
+ onSave?: (entry: ContentEntry) => void
112
+ onCancel?: () => void
113
+ onDelete?: () => void
114
+ autosaveEnabled?: boolean
115
+ autosaveInterval?: number
116
+ canDelete?: boolean
117
+ }
118
+
119
+ export function ContentEntryEditor({
120
+ contentType,
121
+ entry,
122
+ onSave,
123
+ onCancel,
124
+ onDelete,
125
+ autosaveEnabled = true,
126
+ autosaveInterval = 30000,
127
+ canDelete: canDeleteProp = false,
128
+ }: ContentEntryEditorProps) {
129
+ const { settings } = useSettingsConfig()
130
+
131
+ const getInitialData = useCallback(() => {
132
+ if (entry) {
133
+ return transformDataForUI({ ...entry.data }, contentType.fields)
134
+ }
135
+
136
+ const defaults: Record<string, unknown> = {}
137
+ for (const field of contentType.fields) {
138
+ if (field.defaultValue !== undefined) {
139
+ defaults[field.name] = field.defaultValue
140
+ } else {
141
+ switch (field.type) {
142
+ case 'text':
143
+ case 'richText':
144
+ defaults[field.name] = ''
145
+ break
146
+ case 'boolean':
147
+ defaults[field.name] = false
148
+ break
149
+ case 'number':
150
+ case 'date':
151
+ case 'datetime':
152
+ case 'reference':
153
+ case 'media':
154
+ case 'json':
155
+ defaults[field.name] = null
156
+ break
157
+ case 'select':
158
+ defaults[field.name] = ''
159
+ break
160
+ case 'multiSelect':
161
+ defaults[field.name] = []
162
+ break
163
+ }
164
+ }
165
+ }
166
+ return defaults
167
+ }, [contentType.fields, entry])
168
+
169
+ const [formData, setFormData] =
170
+ useState<Record<string, unknown>>(getInitialData)
171
+ const [fieldErrors, setFieldErrors] = useState<Record<string, FieldError>>({})
172
+ const [isSubmitting, setIsSubmitting] = useState(false)
173
+ const [isDirty, setIsDirty] = useState(false)
174
+ const [_lastSavedData, setLastSavedData] = useState<Record<
175
+ string,
176
+ unknown
177
+ > | null>(entry ? { ...entry.data } : null)
178
+ const [autosaveStatus, setAutosaveStatus] = useState<
179
+ 'idle' | 'saving' | 'saved' | 'error'
180
+ >('idle')
181
+ const [autosaveError, setAutosaveError] = useState<string | null>(null)
182
+ const [autosaveRetryCount, setAutosaveRetryCount] = useState(0)
183
+ const [submitError, setSubmitError] = useState<string | null>(null)
184
+ const [saveSuccess, setSaveSuccess] = useState(false)
185
+ const maxAutosaveRetries = 3
186
+
187
+ const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
188
+ const formDataRef = useRef(formData)
189
+ formDataRef.current = formData
190
+
191
+ const createEntry = useMutation(api.entries.create)
192
+ const updateEntry = useMutation(api.entries.update)
193
+ const publishEntry = useMutation(api.entries.publish)
194
+ const unpublishEntry = useMutation(api.entries.unpublish)
195
+ const scheduleEntry = useMutation(api.entries.schedule)
196
+ const cancelScheduleEntry = useMutation(api.entries.cancelSchedule)
197
+ const deleteEntryMutation = useMutation(api.entries.remove)
198
+ const duplicateEntryMutation = useMutation(api.entries.duplicate)
199
+
200
+ const [showScheduleModal, setShowScheduleModal] = useState(false)
201
+ const [scheduleDateTime, setScheduleDateTime] = useState<string>(() => {
202
+ const tomorrow = new Date()
203
+ tomorrow.setDate(tomorrow.getDate() + 1)
204
+ tomorrow.setHours(9, 0, 0, 0)
205
+ return formatDateTimeLocal(tomorrow.getTime())
206
+ })
207
+ const [isPublishing, setIsPublishing] = useState(false)
208
+ const [publishError, setPublishError] = useState<string | null>(null)
209
+
210
+ useEffect(() => {
211
+ const newData = getInitialData()
212
+ setFormData(newData)
213
+ setFieldErrors({})
214
+ setIsDirty(false)
215
+ setLastSavedData(entry ? { ...entry.data } : null)
216
+ setSubmitError(null)
217
+ }, [entry?._id, getInitialData])
218
+
219
+ const handleFieldChange = useCallback((fieldName: string, value: unknown) => {
220
+ setFormData((prev) => {
221
+ const updated = { ...prev, [fieldName]: value }
222
+ return updated
223
+ })
224
+ setIsDirty(true)
225
+
226
+ setFieldErrors((prev) => {
227
+ if (prev[fieldName]) {
228
+ const { [fieldName]: _removed, ...rest } = prev
229
+ return rest
230
+ }
231
+ return prev
232
+ })
233
+ }, [])
234
+
235
+ const validateForm = useCallback(async (): Promise<boolean> => {
236
+ const errors: Record<string, FieldError> = {}
237
+
238
+ for (const field of contentType.fields) {
239
+ const value = formData[field.name]
240
+
241
+ if (field.required) {
242
+ const isEmpty =
243
+ value === null ||
244
+ value === undefined ||
245
+ value === '' ||
246
+ (Array.isArray(value) && value.length === 0)
247
+
248
+ if (isEmpty) {
249
+ errors[field.name] = {
250
+ message: `${field.label} is required`,
251
+ code: 'REQUIRED',
252
+ }
253
+ continue
254
+ }
255
+ }
256
+
257
+ if (value !== null && value !== undefined && value !== '') {
258
+ switch (field.type) {
259
+ case 'text': {
260
+ const strValue = String(value)
261
+ const opts = field.options
262
+ if (opts?.minLength && strValue.length < opts.minLength) {
263
+ errors[field.name] = {
264
+ message: `Minimum ${opts.minLength} characters required`,
265
+ code: 'MIN_LENGTH',
266
+ }
267
+ } else if (opts?.maxLength && strValue.length > opts.maxLength) {
268
+ errors[field.name] = {
269
+ message: `Maximum ${opts.maxLength} characters allowed`,
270
+ code: 'MAX_LENGTH',
271
+ }
272
+ } else if (opts?.pattern) {
273
+ const regex = new RegExp(opts.pattern)
274
+ if (!regex.test(strValue)) {
275
+ errors[field.name] = {
276
+ message: 'Value does not match the required format',
277
+ code: 'PATTERN_MISMATCH',
278
+ }
279
+ }
280
+ }
281
+ break
282
+ }
283
+
284
+ case 'number': {
285
+ const numValue = Number(value)
286
+ const opts = field.options
287
+ if (isNaN(numValue)) {
288
+ errors[field.name] = {
289
+ message: 'Must be a valid number',
290
+ code: 'INVALID_TYPE',
291
+ }
292
+ } else {
293
+ if (opts?.min !== undefined && numValue < opts.min) {
294
+ errors[field.name] = {
295
+ message: `Minimum value is ${opts.min}`,
296
+ code: 'MIN_VALUE',
297
+ }
298
+ } else if (opts?.max !== undefined && numValue > opts.max) {
299
+ errors[field.name] = {
300
+ message: `Maximum value is ${opts.max}`,
301
+ code: 'MAX_VALUE',
302
+ }
303
+ } else if (opts?.precision === 0 && !Number.isInteger(numValue)) {
304
+ errors[field.name] = {
305
+ message: 'Must be a whole number',
306
+ code: 'NOT_INTEGER',
307
+ }
308
+ }
309
+ }
310
+ break
311
+ }
312
+
313
+ case 'select': {
314
+ const opts = field.options
315
+ if (opts?.options && !opts.options.some((o) => o.value === value)) {
316
+ errors[field.name] = {
317
+ message: 'Please select a valid option',
318
+ code: 'INVALID_OPTION',
319
+ }
320
+ }
321
+ break
322
+ }
323
+
324
+ case 'multiSelect': {
325
+ const opts = field.options
326
+ if (Array.isArray(value) && opts?.options) {
327
+ const validValues = opts.options.map((o) => o.value)
328
+ const invalid = value.filter(
329
+ (v) => !validValues.includes(String(v))
330
+ )
331
+ if (invalid.length > 0) {
332
+ errors[field.name] = {
333
+ message: 'Contains invalid options',
334
+ code: 'INVALID_OPTION',
335
+ }
336
+ }
337
+ }
338
+ break
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ setFieldErrors(errors)
345
+ return Object.keys(errors).length === 0
346
+ }, [contentType.fields, formData])
347
+
348
+ const autosaveDraft = useCallback(
349
+ async (retryAttempt = 0) => {
350
+ if (!isDirty || !entry) return
351
+
352
+ if (entry.status !== 'draft') return
353
+
354
+ try {
355
+ setAutosaveStatus('saving')
356
+ setAutosaveError(null)
357
+
358
+ await updateEntry({
359
+ id: entry._id,
360
+ data: transformDataForBackend(formDataRef.current, contentType.fields),
361
+ })
362
+
363
+ setLastSavedData({ ...formDataRef.current })
364
+ setAutosaveStatus('saved')
365
+ setIsDirty(false)
366
+ setAutosaveRetryCount(0)
367
+
368
+ setTimeout(() => {
369
+ setAutosaveStatus('idle')
370
+ }, 3000)
371
+ } catch (error) {
372
+ console.error('Autosave failed:', error)
373
+
374
+ const errorMessage =
375
+ error instanceof Error ? error.message : 'Failed to save'
376
+ const canRetry =
377
+ isRetryableError(error) && retryAttempt < maxAutosaveRetries
378
+
379
+ if (canRetry) {
380
+ const retryDelay = Math.min(1000 * Math.pow(2, retryAttempt), 10000)
381
+ setAutosaveStatus('error')
382
+ setAutosaveError(
383
+ `Save failed, retrying in ${Math.round(retryDelay / 1000)}s...`
384
+ )
385
+ setAutosaveRetryCount(retryAttempt + 1)
386
+
387
+ setTimeout(() => {
388
+ autosaveDraft(retryAttempt + 1)
389
+ }, retryDelay)
390
+ } else {
391
+ setAutosaveStatus('error')
392
+ setAutosaveError(errorMessage)
393
+ setAutosaveRetryCount(0)
394
+ }
395
+ }
396
+ },
397
+ [isDirty, entry, updateEntry, maxAutosaveRetries]
398
+ )
399
+
400
+ const handleAutosaveRetry = useCallback(() => {
401
+ setAutosaveRetryCount(0)
402
+ autosaveDraft(0)
403
+ }, [autosaveDraft])
404
+
405
+ useEffect(() => {
406
+ if (!autosaveEnabled || !entry || entry.status !== 'draft') {
407
+ return
408
+ }
409
+
410
+ if (autosaveTimerRef.current) {
411
+ clearTimeout(autosaveTimerRef.current)
412
+ }
413
+
414
+ if (isDirty) {
415
+ autosaveTimerRef.current = setTimeout(() => {
416
+ autosaveDraft()
417
+ }, autosaveInterval)
418
+ }
419
+
420
+ return () => {
421
+ if (autosaveTimerRef.current) {
422
+ clearTimeout(autosaveTimerRef.current)
423
+ }
424
+ }
425
+ }, [autosaveEnabled, autosaveInterval, isDirty, entry, autosaveDraft])
426
+
427
+ const [showConfirmModal, setShowConfirmModal] = useState(false)
428
+ const [confirmAction, setConfirmAction] = useState<
429
+ 'publish' | 'unpublish' | null
430
+ >(null)
431
+
432
+ const [showDeleteModal, setShowDeleteModal] = useState(false)
433
+ const [isDeleting, setIsDeleting] = useState(false)
434
+ const [deleteError, setDeleteError] = useState<string | null>(null)
435
+
436
+ const [isDuplicating, setIsDuplicating] = useState(false)
437
+ const [isArchiving, setIsArchiving] = useState(false)
438
+ const [showVersionHistory, setShowVersionHistory] = useState(false)
439
+
440
+ const handlePublishClick = useCallback(() => {
441
+ setConfirmAction('publish')
442
+ setShowConfirmModal(true)
443
+ }, [])
444
+
445
+ const handleUnpublishClick = useCallback(() => {
446
+ setConfirmAction('unpublish')
447
+ setShowConfirmModal(true)
448
+ }, [])
449
+
450
+ const handleConfirmAction = useCallback(async () => {
451
+ if (!entry || !confirmAction) return
452
+
453
+ setShowConfirmModal(false)
454
+ setIsPublishing(true)
455
+ setPublishError(null)
456
+
457
+ try {
458
+ if (confirmAction === 'publish') {
459
+ const publishedEntry = (await publishEntry({
460
+ id: entry._id,
461
+ changeDescription: 'Published from editor',
462
+ })) as ContentEntry
463
+ onSave?.(publishedEntry)
464
+ } else {
465
+ const draftEntry = (await unpublishEntry({
466
+ id: entry._id,
467
+ })) as ContentEntry
468
+ onSave?.(draftEntry)
469
+ }
470
+ } catch (error) {
471
+ const message =
472
+ error instanceof Error
473
+ ? error.message
474
+ : `Failed to ${confirmAction}`
475
+ setPublishError(message)
476
+ } finally {
477
+ setIsPublishing(false)
478
+ setConfirmAction(null)
479
+ }
480
+ }, [entry, confirmAction, publishEntry, unpublishEntry, onSave])
481
+
482
+ const handlePublish = useCallback(async () => {
483
+ if (!entry) return
484
+
485
+ setIsPublishing(true)
486
+ setPublishError(null)
487
+
488
+ try {
489
+ const publishedEntry = (await publishEntry({
490
+ id: entry._id,
491
+ changeDescription: 'Published from editor',
492
+ })) as ContentEntry
493
+ onSave?.(publishedEntry)
494
+ } catch (error) {
495
+ const message =
496
+ error instanceof Error ? error.message : 'Failed to publish'
497
+ setPublishError(message)
498
+ } finally {
499
+ setIsPublishing(false)
500
+ }
501
+ }, [entry, publishEntry, onSave])
502
+
503
+ const _handleUnpublish = useCallback(async () => {
504
+ if (!entry) return
505
+
506
+ setIsPublishing(true)
507
+ setPublishError(null)
508
+
509
+ try {
510
+ const draftEntry = (await unpublishEntry({
511
+ id: entry._id,
512
+ })) as ContentEntry
513
+ onSave?.(draftEntry)
514
+ } catch (error) {
515
+ const message =
516
+ error instanceof Error ? error.message : 'Failed to unpublish'
517
+ setPublishError(message)
518
+ } finally {
519
+ setIsPublishing(false)
520
+ }
521
+ }, [entry, unpublishEntry, onSave])
522
+
523
+ const handleSchedule = useCallback(async () => {
524
+ if (!entry) return
525
+
526
+ const publishAt = parseDateTimeLocal(scheduleDateTime)
527
+ const minimumTime = Date.now() + 60 * 1000
528
+
529
+ if (publishAt < minimumTime) {
530
+ setPublishError('Schedule time must be at least 1 minute in the future')
531
+ return
532
+ }
533
+
534
+ setIsPublishing(true)
535
+ setPublishError(null)
536
+
537
+ try {
538
+ const scheduledEntry = (await scheduleEntry({
539
+ id: entry._id,
540
+ publishAt,
541
+ })) as ContentEntry
542
+ setShowScheduleModal(false)
543
+ onSave?.(scheduledEntry)
544
+ } catch (error) {
545
+ const message =
546
+ error instanceof Error ? error.message : 'Failed to schedule'
547
+ setPublishError(message)
548
+ } finally {
549
+ setIsPublishing(false)
550
+ }
551
+ }, [entry, scheduleDateTime, scheduleEntry, onSave])
552
+
553
+ const handleCancelSchedule = useCallback(async () => {
554
+ if (!entry) return
555
+
556
+ setIsPublishing(true)
557
+ setPublishError(null)
558
+
559
+ try {
560
+ const draftEntry = (await cancelScheduleEntry({
561
+ id: entry._id,
562
+ })) as ContentEntry
563
+ onSave?.(draftEntry)
564
+ } catch (error) {
565
+ const message =
566
+ error instanceof Error ? error.message : 'Failed to cancel schedule'
567
+ setPublishError(message)
568
+ } finally {
569
+ setIsPublishing(false)
570
+ }
571
+ }, [entry, cancelScheduleEntry, onSave])
572
+
573
+ const handleDeleteClick = useCallback(() => {
574
+ setDeleteError(null)
575
+ setShowDeleteModal(true)
576
+ }, [])
577
+
578
+ const handleDeleteConfirm = useCallback(async () => {
579
+ if (!entry) return
580
+
581
+ setIsDeleting(true)
582
+ setDeleteError(null)
583
+
584
+ try {
585
+ await deleteEntryMutation({
586
+ id: entry._id,
587
+ hardDelete: false,
588
+ })
589
+ setShowDeleteModal(false)
590
+ onDelete?.()
591
+ } catch (error) {
592
+ const message =
593
+ error instanceof Error ? error.message : 'Failed to delete entry'
594
+ setDeleteError(message)
595
+ } finally {
596
+ setIsDeleting(false)
597
+ }
598
+ }, [entry, deleteEntryMutation, onDelete])
599
+
600
+ const handleDuplicate = useCallback(async () => {
601
+ if (!entry) return
602
+
603
+ setIsDuplicating(true)
604
+ setSubmitError(null)
605
+
606
+ try {
607
+ const duplicatedEntry = (await duplicateEntryMutation({
608
+ sourceEntryId: entry._id,
609
+ })) as ContentEntry
610
+ onSave?.(duplicatedEntry)
611
+ } catch (error) {
612
+ const message =
613
+ error instanceof Error ? error.message : 'Failed to duplicate entry'
614
+ setSubmitError(message)
615
+ } finally {
616
+ setIsDuplicating(false)
617
+ }
618
+ }, [entry, duplicateEntryMutation, onSave])
619
+
620
+ const handleArchive = useCallback(async () => {
621
+ if (!entry) return
622
+
623
+ setIsArchiving(true)
624
+ setSubmitError(null)
625
+
626
+ try {
627
+ const archivedEntry = (await updateEntry({
628
+ id: entry._id,
629
+ status: 'archived',
630
+ })) as ContentEntry
631
+ onSave?.(archivedEntry)
632
+ } catch (error) {
633
+ const message =
634
+ error instanceof Error ? error.message : 'Failed to archive entry'
635
+ setSubmitError(message)
636
+ } finally {
637
+ setIsArchiving(false)
638
+ }
639
+ }, [entry, updateEntry, onSave])
640
+
641
+ const handleSubmit = useCallback(
642
+ async (e: React.FormEvent) => {
643
+ e.preventDefault()
644
+ setSubmitError(null)
645
+
646
+ const isValid = await validateForm()
647
+ if (!isValid) {
648
+ return
649
+ }
650
+
651
+ setIsSubmitting(true)
652
+
653
+ try {
654
+ let savedEntry: ContentEntry
655
+
656
+ const dataForBackend = transformDataForBackend(
657
+ formData,
658
+ contentType.fields
659
+ )
660
+
661
+ if (entry) {
662
+ savedEntry = (await updateEntry({
663
+ id: entry._id,
664
+ data: dataForBackend,
665
+ })) as ContentEntry
666
+ } else {
667
+ savedEntry = (await createEntry({
668
+ contentTypeId: contentType._id,
669
+ data: dataForBackend,
670
+ })) as ContentEntry
671
+ }
672
+
673
+ setIsDirty(false)
674
+ setLastSavedData({ ...formData })
675
+ setSaveSuccess(true)
676
+ setTimeout(() => setSaveSuccess(false), 3000)
677
+ onSave?.(savedEntry)
678
+ } catch (error) {
679
+ const { fieldErrors: serverFieldErrors, generalError } =
680
+ parseServerError(error)
681
+ const message =
682
+ generalError ??
683
+ (error instanceof Error ? error.message : 'Failed to save entry')
684
+
685
+ setSubmitError(message)
686
+
687
+ if (Object.keys(serverFieldErrors).length > 0) {
688
+ setFieldErrors((prev) => ({ ...prev, ...serverFieldErrors }))
689
+ }
690
+ } finally {
691
+ setIsSubmitting(false)
692
+ }
693
+ },
694
+ [
695
+ validateForm,
696
+ entry,
697
+ formData,
698
+ contentType._id,
699
+ createEntry,
700
+ updateEntry,
701
+ onSave,
702
+ ]
703
+ )
704
+
705
+ const handleCancel = useCallback(() => {
706
+ if (isDirty) {
707
+ const confirmed = window.confirm(
708
+ 'You have unsaved changes. Are you sure you want to leave?'
709
+ )
710
+ if (!confirmed) return
711
+ }
712
+ onCancel?.()
713
+ }, [isDirty, onCancel])
714
+
715
+ const getAutosaveStatusText = () => {
716
+ switch (autosaveStatus) {
717
+ case 'saving':
718
+ return autosaveRetryCount > 0
719
+ ? `Retrying (${autosaveRetryCount}/${maxAutosaveRetries})...`
720
+ : 'Saving...'
721
+ case 'saved':
722
+ return 'Draft saved'
723
+ case 'error':
724
+ return autosaveError ?? 'Autosave failed'
725
+ default:
726
+ return null
727
+ }
728
+ }
729
+
730
+ return (
731
+ <form className="space-y-6" onSubmit={handleSubmit}>
732
+ {/* Header */}
733
+ <div className="flex flex-wrap items-center justify-between gap-4">
734
+ <div className="flex items-center gap-3">
735
+ <h2 className="text-xl font-semibold">
736
+ {entry ? 'Edit' : 'Create'} {contentType.displayName}
737
+ </h2>
738
+ {entry && <CmsStatusBadge status={entry.status} />}
739
+ </div>
740
+
741
+ <div className="flex items-center gap-3">
742
+ {autosaveStatus !== 'idle' && (
743
+ <div className="flex items-center gap-2">
744
+ <span
745
+ className={cn(
746
+ 'flex items-center gap-1.5 text-sm',
747
+ autosaveStatus === 'saving' && 'text-muted-foreground',
748
+ autosaveStatus === 'saved' && 'text-emerald-600',
749
+ autosaveStatus === 'error' && 'text-red-600'
750
+ )}
751
+ data-testid="autosave-status"
752
+ >
753
+ {autosaveStatus === 'saving' && (
754
+ <Loader2 className="size-3 animate-spin" />
755
+ )}
756
+ {autosaveStatus === 'saved' && (
757
+ <CheckCircle className="size-3" />
758
+ )}
759
+ {autosaveStatus === 'error' && (
760
+ <AlertCircle className="size-3" />
761
+ )}
762
+ {getAutosaveStatusText()}
763
+ </span>
764
+ {autosaveStatus === 'error' && autosaveRetryCount === 0 && (
765
+ <button
766
+ type="button"
767
+ onClick={handleAutosaveRetry}
768
+ className="text-sm text-primary hover:underline"
769
+ data-testid="autosave-retry-button"
770
+ >
771
+ <RefreshCw className="size-3" />
772
+ </button>
773
+ )}
774
+ </div>
775
+ )}
776
+
777
+ {isDirty && (
778
+ <span className="text-sm text-amber-600">Unsaved changes</span>
779
+ )}
780
+ </div>
781
+ </div>
782
+
783
+ {/* Success/Error Messages */}
784
+ {saveSuccess && (
785
+ <div
786
+ className="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800"
787
+ role="status"
788
+ >
789
+ <CheckCircle className="size-4" />
790
+ Changes saved successfully
791
+ </div>
792
+ )}
793
+
794
+ {(submitError || publishError) && (
795
+ <div
796
+ className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800"
797
+ role="alert"
798
+ >
799
+ <span className="font-medium">Error:</span> {submitError || publishError}
800
+ </div>
801
+ )}
802
+
803
+ {/* Fields */}
804
+ <div className="space-y-4">
805
+ {contentType.fields.map((field) => (
806
+ <FieldRenderer
807
+ key={field.name}
808
+ field={field}
809
+ value={formData[field.name]}
810
+ onChange={(value) => handleFieldChange(field.name, value)}
811
+ error={fieldErrors[field.name]}
812
+ disabled={isSubmitting}
813
+ />
814
+ ))}
815
+ </div>
816
+
817
+ {/* Footer */}
818
+ <div className="flex flex-wrap items-center justify-between gap-4 border-t pt-4">
819
+ <div className="flex flex-wrap items-center gap-2">
820
+ {entry && canDeleteProp && (
821
+ <CmsButton
822
+ type="button"
823
+ variant="danger"
824
+ onClick={handleDeleteClick}
825
+ disabled={
826
+ isSubmitting ||
827
+ isPublishing ||
828
+ isDeleting ||
829
+ isDuplicating
830
+ }
831
+ loading={isDeleting}
832
+ data-testid="delete-button"
833
+ >
834
+ Delete
835
+ </CmsButton>
836
+ )}
837
+
838
+ {entry && (
839
+ <CmsButton
840
+ type="button"
841
+ variant="secondary"
842
+ onClick={handleDuplicate}
843
+ disabled={
844
+ isSubmitting ||
845
+ isPublishing ||
846
+ isDeleting ||
847
+ isDuplicating ||
848
+ isArchiving
849
+ }
850
+ loading={isDuplicating}
851
+ data-testid="duplicate-button"
852
+ >
853
+ Duplicate
854
+ </CmsButton>
855
+ )}
856
+
857
+ {entry &&
858
+ (entry.status === 'draft' || entry.status === 'scheduled') && (
859
+ <CmsButton
860
+ type="button"
861
+ variant="secondary"
862
+ onClick={handleArchive}
863
+ disabled={
864
+ isSubmitting ||
865
+ isPublishing ||
866
+ isDeleting ||
867
+ isDuplicating ||
868
+ isArchiving
869
+ }
870
+ loading={isArchiving}
871
+ data-testid="archive-button"
872
+ >
873
+ Archive
874
+ </CmsButton>
875
+ )}
876
+
877
+ {entry && (
878
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
879
+ {settings?.features.versioning && (
880
+ <button
881
+ type="button"
882
+ onClick={() => setShowVersionHistory(true)}
883
+ className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-1.5 transition-colors hover:bg-muted/50"
884
+ >
885
+ <History className="size-4 text-muted-foreground" />
886
+ <span className="text-xs font-medium text-foreground">
887
+ Version {entry.version}
888
+ </span>
889
+ <span className="text-xs text-muted-foreground">
890
+ View history
891
+ </span>
892
+ </button>
893
+ )}
894
+
895
+ {entry.lastPublishedAt && (
896
+ <span
897
+ className="text-xs"
898
+ data-testid="last-published-time"
899
+ >
900
+ Last published:{' '}
901
+ {new Date(entry.lastPublishedAt).toLocaleString()}
902
+ </span>
903
+ )}
904
+
905
+ {settings?.features.scheduling &&
906
+ entry.status === 'scheduled' &&
907
+ entry.scheduledPublishAt && (
908
+ <span
909
+ className="flex items-center gap-1 text-xs text-blue-600"
910
+ data-testid="scheduled-time"
911
+ >
912
+ <Clock className="size-3" />
913
+ Scheduled:{' '}
914
+ {new Date(entry.scheduledPublishAt).toLocaleString()}
915
+ </span>
916
+ )}
917
+ </div>
918
+ )}
919
+ </div>
920
+
921
+ <div className="flex flex-wrap items-center gap-2">
922
+ <CmsButton
923
+ type="button"
924
+ variant="outline"
925
+ onClick={handleCancel}
926
+ disabled={isSubmitting || isPublishing}
927
+ >
928
+ Cancel
929
+ </CmsButton>
930
+
931
+ <CmsButton
932
+ type="submit"
933
+ variant="primary"
934
+ disabled={isSubmitting || isPublishing}
935
+ loading={isSubmitting}
936
+ >
937
+ {entry ? 'Save Changes' : 'Create Entry'}
938
+ </CmsButton>
939
+
940
+ {entry && (
941
+ <>
942
+ {entry.status === 'draft' && (
943
+ <>
944
+ {settings?.features.scheduling && (
945
+ <CmsButton
946
+ type="button"
947
+ variant="secondary"
948
+ onClick={() => setShowScheduleModal(true)}
949
+ disabled={isSubmitting || isPublishing}
950
+ >
951
+ Schedule
952
+ </CmsButton>
953
+ )}
954
+ <CmsButton
955
+ type="button"
956
+ variant="success"
957
+ onClick={handlePublishClick}
958
+ disabled={isSubmitting || isPublishing}
959
+ loading={isPublishing}
960
+ data-testid="publish-button"
961
+ >
962
+ Publish Now
963
+ </CmsButton>
964
+ </>
965
+ )}
966
+
967
+ {settings?.features.scheduling && entry.status === 'scheduled' && (
968
+ <>
969
+ <CmsButton
970
+ type="button"
971
+ variant="secondary"
972
+ onClick={handleCancelSchedule}
973
+ disabled={isSubmitting || isPublishing}
974
+ >
975
+ Cancel Schedule
976
+ </CmsButton>
977
+ <CmsButton
978
+ type="button"
979
+ variant="success"
980
+ onClick={handlePublish}
981
+ disabled={isSubmitting || isPublishing}
982
+ loading={isPublishing}
983
+ >
984
+ Publish Now
985
+ </CmsButton>
986
+ </>
987
+ )}
988
+
989
+ {entry.status === 'published' && (
990
+ <CmsButton
991
+ type="button"
992
+ variant="warning"
993
+ onClick={handleUnpublishClick}
994
+ disabled={isSubmitting || isPublishing}
995
+ loading={isPublishing}
996
+ data-testid="unpublish-button"
997
+ >
998
+ Unpublish
999
+ </CmsButton>
1000
+ )}
1001
+ </>
1002
+ )}
1003
+ </div>
1004
+ </div>
1005
+
1006
+ {/* Schedule Modal */}
1007
+ {settings?.features.scheduling && (
1008
+ <CmsDialog
1009
+ open={showScheduleModal}
1010
+ onOpenChange={setShowScheduleModal}
1011
+ title="Schedule Publication"
1012
+ size="sm"
1013
+ footer={
1014
+ <>
1015
+ <CmsButton
1016
+ variant="outline"
1017
+ onClick={() => setShowScheduleModal(false)}
1018
+ >
1019
+ Cancel
1020
+ </CmsButton>
1021
+ <CmsButton
1022
+ variant="primary"
1023
+ onClick={handleSchedule}
1024
+ loading={isPublishing}
1025
+ >
1026
+ Schedule
1027
+ </CmsButton>
1028
+ </>
1029
+ }
1030
+ >
1031
+ <div className="space-y-4">
1032
+ <p className="text-sm text-muted-foreground">
1033
+ Choose when this content should be automatically published:
1034
+ </p>
1035
+ <Input
1036
+ type="datetime-local"
1037
+ value={scheduleDateTime}
1038
+ onChange={(e) => setScheduleDateTime(e.target.value)}
1039
+ min={formatDateTimeLocal(Date.now() + 60 * 1000)}
1040
+ />
1041
+ {publishError && (
1042
+ <p className="text-sm text-destructive">{publishError}</p>
1043
+ )}
1044
+ </div>
1045
+ </CmsDialog>
1046
+ )}
1047
+
1048
+ {/* Publish/Unpublish Confirmation Modal */}
1049
+ <CmsConfirmDialog
1050
+ open={showConfirmModal && confirmAction !== null}
1051
+ onOpenChange={(open) => {
1052
+ if (!open) {
1053
+ setShowConfirmModal(false)
1054
+ setConfirmAction(null)
1055
+ }
1056
+ }}
1057
+ title={
1058
+ confirmAction === 'publish' ? 'Confirm Publish' : 'Confirm Unpublish'
1059
+ }
1060
+ description={
1061
+ confirmAction === 'publish'
1062
+ ? 'Are you sure you want to publish this entry? It will become publicly visible.'
1063
+ : 'Are you sure you want to unpublish this entry? It will no longer be publicly visible.'
1064
+ }
1065
+ confirmLabel={confirmAction === 'publish' ? 'Publish' : 'Unpublish'}
1066
+ variant={confirmAction === 'publish' ? 'primary' : 'warning'}
1067
+ onConfirm={handleConfirmAction}
1068
+ isLoading={isPublishing}
1069
+ />
1070
+
1071
+ {/* Delete Confirmation Modal */}
1072
+ <CmsConfirmDialog
1073
+ open={showDeleteModal}
1074
+ onOpenChange={(open) => {
1075
+ if (!open && !isDeleting) {
1076
+ setShowDeleteModal(false)
1077
+ setDeleteError(null)
1078
+ }
1079
+ }}
1080
+ title="Delete Entry"
1081
+ description="Are you sure you want to delete this entry? It will be moved to the trash and can be restored within the retention period."
1082
+ confirmLabel="Delete"
1083
+ variant="danger"
1084
+ onConfirm={handleDeleteConfirm}
1085
+ isLoading={isDeleting}
1086
+ error={deleteError}
1087
+ />
1088
+
1089
+ {/* Version History Panel */}
1090
+ {settings?.features.versioning && showVersionHistory && entry && (
1091
+ <div className="fixed inset-y-0 right-0 z-50 flex w-96 flex-col shadow-xl">
1092
+ <VersionHistory
1093
+ entryId={entry._id}
1094
+ currentVersion={entry.version}
1095
+ onRollbackComplete={() => {
1096
+ setShowVersionHistory(false)
1097
+ }}
1098
+ onClose={() => setShowVersionHistory(false)}
1099
+ />
1100
+ </div>
1101
+ )}
1102
+ </form>
1103
+ )
1104
+ }