convex-cms 0.0.5-alpha.0 → 0.0.5-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/README.md +95 -144
  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-CiMQwSQV.js → CmsEmptyState-CkqBIab3.js} +1 -1
  158. package/admin-dist/public/assets/{CmsPageHeader-ohOq0luT.js → CmsPageHeader-CUtl5MMG.js} +1 -1
  159. package/admin-dist/public/assets/{CmsStatusBadge-BdNf0V9v.js → CmsStatusBadge-CUYFgEe-.js} +1 -1
  160. package/admin-dist/public/assets/{CmsSurface-CWup6Jh7.js → CmsSurface-CsJfAVa3.js} +1 -1
  161. package/admin-dist/public/assets/{CmsToolbar-cEBlCHa3.js → CmsToolbar-CnfbcxeP.js} +1 -1
  162. package/admin-dist/public/assets/{ContentEntryEditor-BY5ypfUs.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-BpSmrfAm.js → _entryId-CuVMExbb.js} +1 -1
  166. package/admin-dist/public/assets/{alert-Bf2l8kxw.js → alert-CF1BSzGR.js} +1 -1
  167. package/admin-dist/public/assets/{badge-qPrc4AUM.js → badge-CmuOIVKp.js} +1 -1
  168. package/admin-dist/public/assets/{circle-check-big-Dgozy3vV.js → circle-check-big-BKDVG6DU.js} +1 -1
  169. package/admin-dist/public/assets/{command-QOmNhlb0.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-DCsUdvFh.js → label-CHCnXeBk.js} +1 -1
  175. package/admin-dist/public/assets/{link-2-Czw1N61H.js → link-2-Bb34judH.js} +1 -1
  176. package/admin-dist/public/assets/{list-DtCsXj8-.js → list-9Pzt48ld.js} +1 -1
  177. package/admin-dist/public/assets/{main-CXgkZMhe.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-CoTDxKzf.js → new._contentTypeId-C_I4YxIa.js} +1 -1
  180. package/admin-dist/public/assets/{plus-xCFJK0RC.js → plus-Ceef7DHk.js} +1 -1
  181. package/admin-dist/public/assets/{rotate-ccw-DIqK63wY.js → rotate-ccw-7k7-4VUq.js} +1 -1
  182. package/admin-dist/public/assets/{scroll-area-B-yrE66a.js → scroll-area-CC6wujnp.js} +1 -1
  183. package/admin-dist/public/assets/{search-CbCbboeU.js → search-DwoUV2pv.js} +1 -1
  184. package/admin-dist/public/assets/{select-Co3TZFJb.js → select-hOZTp8aC.js} +1 -1
  185. package/admin-dist/public/assets/{settings-BspTTv_o.js → settings-t2PbCZh4.js} +1 -1
  186. package/admin-dist/public/assets/{switch-CfavASmR.js → switch-jX2pDaNU.js} +1 -1
  187. package/admin-dist/public/assets/{tabs-CN5s5u2W.js → tabs-q4EbZk7c.js} +1 -1
  188. package/admin-dist/public/assets/{tanstack-adapter-npeE3RdY.js → tanstack-adapter-B-Glm4kH.js} +1 -1
  189. package/admin-dist/public/assets/taxonomies-kyk5P4ZW.js +1 -0
  190. package/admin-dist/public/assets/{textarea-BJ0XFZpT.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-BZRcqsUg.js → triangle-alert-CXFIO_Gu.js} +1 -1
  193. package/admin-dist/public/assets/{useBreadcrumbLabel-DwZlwvFF.js → useBreadcrumbLabel-_6qBagc3.js} +1 -1
  194. package/admin-dist/public/assets/{usePermissions-C1JQhfqb.js → usePermissions-M1ijZ7a6.js} +1 -1
  195. package/admin-dist/server/_ssr/{CmsButton-B45JAKR1.mjs → CmsButton-DOiTVKQq.mjs} +1 -1
  196. package/admin-dist/server/_ssr/{CmsEmptyState-D_BQFAVR.mjs → CmsEmptyState-fbnGt3LD.mjs} +2 -2
  197. package/admin-dist/server/_ssr/{CmsPageHeader-CrUZA59A.mjs → CmsPageHeader-DHRrdOZa.mjs} +1 -1
  198. package/admin-dist/server/_ssr/{CmsStatusBadge-B-sj6yaj.mjs → CmsStatusBadge-s7obWbKZ.mjs} +2 -2
  199. package/admin-dist/server/_ssr/{CmsSurface-DKJZhpjk.mjs → CmsSurface-rFoYjb62.mjs} +1 -1
  200. package/admin-dist/server/_ssr/{CmsToolbar-ByaW5iXf.mjs → CmsToolbar-zTE45z2q.mjs} +2 -2
  201. package/admin-dist/server/_ssr/{ContentEntryEditor-D3_Jb1dq.mjs → ContentEntryEditor-BLoEjT_m.mjs} +12 -12
  202. package/admin-dist/server/_ssr/{TaxonomyFilter-BRJkuCtA.mjs → TaxonomyFilter-XAtaJC2z.mjs} +5 -5
  203. package/admin-dist/server/_ssr/{_contentTypeId-B9kA6CaM.mjs → _contentTypeId-Csl4822C.mjs} +13 -13
  204. package/admin-dist/server/_ssr/{_entryId-BddcMkZN.mjs → _entryId-D8alLFBx.mjs} +15 -15
  205. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BffZedId.mjs +4 -0
  206. package/admin-dist/server/_ssr/{command-CGtVr8Gb.mjs → command-C0Di14--.mjs} +1 -1
  207. package/admin-dist/server/_ssr/{content-D1tbeOd0.mjs → content-CT-FPsmV.mjs} +12 -55
  208. package/admin-dist/server/_ssr/{content-types-BZqY_BER.mjs → content-types-C8cBFdzE.mjs} +15 -46
  209. package/admin-dist/server/_ssr/{index-BIdq4xaC.mjs → index-BJtcrEc-.mjs} +5 -5
  210. package/admin-dist/server/_ssr/index.mjs +2 -2
  211. package/admin-dist/server/_ssr/{label-T-QNKAr6.mjs → label-qn2Afwl4.mjs} +1 -1
  212. package/admin-dist/server/_ssr/{media-C-xqjBrl.mjs → media-qv5IAsMZ.mjs} +14 -14
  213. package/admin-dist/server/_ssr/{new._contentTypeId-DWic9cRq.mjs → new._contentTypeId-DdGyrhqs.mjs} +13 -13
  214. package/admin-dist/server/_ssr/{router-D1BMAMJT.mjs → router-nSVkxb6Y.mjs} +11 -11
  215. package/admin-dist/server/_ssr/{scroll-area-C0pic_WA.mjs → scroll-area-BCinP455.mjs} +1 -1
  216. package/admin-dist/server/_ssr/{select-CqmuN2F6.mjs → select-BKQlQScw.mjs} +1 -1
  217. package/admin-dist/server/_ssr/{settings-CAkncGGV.mjs → settings-BCr2KQlk.mjs} +55 -40
  218. package/admin-dist/server/_ssr/{switch-CgmuJkT9.mjs → switch-BaOi42fE.mjs} +1 -1
  219. package/admin-dist/server/_ssr/{tabs-CnMj0aRy.mjs → tabs-DYXEi9kq.mjs} +2 -2
  220. package/admin-dist/server/_ssr/{tanstack-adapter-BXZrMauE.mjs → tanstack-adapter-Bsz8kha-.mjs} +1 -1
  221. package/admin-dist/server/_ssr/{taxonomies-thl3BfVm.mjs → taxonomies-CueMHTbE.mjs} +30 -19
  222. package/admin-dist/server/_ssr/{textarea-4K5OJgeh.mjs → textarea-CI0Jqx2x.mjs} +1 -1
  223. package/admin-dist/server/_ssr/{trash-B40Gx5zP.mjs → trash-DE6W8GoX.mjs} +20 -17
  224. package/admin-dist/server/_ssr/{useBreadcrumbLabel-rn-fL4zV.mjs → useBreadcrumbLabel-B5Yi72lM.mjs} +1 -1
  225. package/admin-dist/server/_ssr/{usePermissions-CKeM6_Vw.mjs → usePermissions-C3nZ-Izm.mjs} +1 -1
  226. package/admin-dist/server/index.mjs +187 -194
  227. package/dist/client/admin/bulk.d.ts +79 -0
  228. package/dist/client/admin/bulk.d.ts.map +1 -0
  229. package/dist/client/admin/bulk.js +72 -0
  230. package/dist/client/admin/bulk.js.map +1 -0
  231. package/dist/client/admin/contentLock.d.ts +118 -0
  232. package/dist/client/admin/contentLock.d.ts.map +1 -0
  233. package/dist/client/admin/contentLock.js +81 -0
  234. package/dist/client/admin/contentLock.js.map +1 -0
  235. package/dist/client/{adminApi.d.ts → admin/contentTypes.d.ts} +39 -1134
  236. package/dist/client/admin/contentTypes.d.ts.map +1 -0
  237. package/dist/client/admin/contentTypes.js +122 -0
  238. package/dist/client/admin/contentTypes.js.map +1 -0
  239. package/dist/client/admin/dashboard.d.ts +16 -0
  240. package/dist/client/admin/dashboard.d.ts.map +1 -0
  241. package/dist/client/admin/dashboard.js +33 -0
  242. package/dist/client/admin/dashboard.js.map +1 -0
  243. package/dist/client/admin/entries.d.ts +358 -0
  244. package/dist/client/admin/entries.d.ts.map +1 -0
  245. package/dist/client/admin/entries.js +220 -0
  246. package/dist/client/admin/entries.js.map +1 -0
  247. package/dist/client/admin/index.d.ts +6568 -0
  248. package/dist/client/admin/index.d.ts.map +1 -0
  249. package/dist/client/admin/index.js +305 -0
  250. package/dist/client/admin/index.js.map +1 -0
  251. package/dist/client/admin/media.d.ts +1038 -0
  252. package/dist/client/admin/media.d.ts.map +1 -0
  253. package/dist/client/admin/media.js +489 -0
  254. package/dist/client/admin/media.js.map +1 -0
  255. package/dist/client/admin/taxonomies.d.ts +339 -0
  256. package/dist/client/admin/taxonomies.d.ts.map +1 -0
  257. package/dist/client/admin/taxonomies.js +364 -0
  258. package/dist/client/admin/taxonomies.js.map +1 -0
  259. package/dist/client/admin/trash.d.ts +91 -0
  260. package/dist/client/admin/trash.d.ts.map +1 -0
  261. package/dist/client/admin/trash.js +71 -0
  262. package/dist/client/admin/trash.js.map +1 -0
  263. package/dist/client/admin/types.d.ts +320 -0
  264. package/dist/client/admin/types.d.ts.map +1 -0
  265. package/dist/client/admin/types.js +7 -0
  266. package/dist/client/admin/types.js.map +1 -0
  267. package/dist/client/admin/validators.d.ts +3886 -0
  268. package/dist/client/admin/validators.d.ts.map +1 -0
  269. package/dist/client/admin/validators.js +322 -0
  270. package/dist/client/admin/validators.js.map +1 -0
  271. package/dist/client/admin/versions.d.ts +106 -0
  272. package/dist/client/admin/versions.d.ts.map +1 -0
  273. package/dist/client/admin/versions.js +57 -0
  274. package/dist/client/admin/versions.js.map +1 -0
  275. package/dist/client/adminApiTypes.d.ts +27 -0
  276. package/dist/client/adminApiTypes.d.ts.map +1 -0
  277. package/dist/client/adminApiTypes.js +12 -0
  278. package/dist/client/adminApiTypes.js.map +1 -0
  279. package/dist/client/{admin-config.d.ts → adminConfig.d.ts} +2 -2
  280. package/dist/client/adminConfig.d.ts.map +1 -0
  281. package/dist/client/{admin-config.js → adminConfig.js} +1 -1
  282. package/dist/client/adminConfig.js.map +1 -0
  283. package/dist/client/agentTools.d.ts +4 -4
  284. package/dist/client/index.d.ts +2 -2
  285. package/dist/client/index.d.ts.map +1 -1
  286. package/dist/client/index.js +15 -2
  287. package/dist/client/index.js.map +1 -1
  288. package/dist/component/contentEntries.d.ts +4 -4
  289. package/dist/component/contentEntryMutations.d.ts +46 -0
  290. package/dist/component/contentEntryMutations.d.ts.map +1 -1
  291. package/dist/component/contentEntryMutations.js +1 -1
  292. package/dist/component/contentEntryMutations.js.map +1 -1
  293. package/dist/component/contentTypeMigration.d.ts +1 -1
  294. package/dist/component/contentTypeMutations.d.ts +22 -0
  295. package/dist/component/contentTypeMutations.d.ts.map +1 -1
  296. package/dist/component/contentTypeMutations.js +1 -1
  297. package/dist/component/contentTypeMutations.js.map +1 -1
  298. package/dist/component/mediaAssetMutations.d.ts +47 -0
  299. package/dist/component/mediaAssetMutations.d.ts.map +1 -1
  300. package/dist/component/mediaAssetMutations.js +1 -1
  301. package/dist/component/mediaAssetMutations.js.map +1 -1
  302. package/dist/component/schema.d.ts +9 -0
  303. package/dist/component/schema.d.ts.map +1 -1
  304. package/dist/component/schema.js +1 -1
  305. package/dist/component/schema.js.map +1 -1
  306. package/package.json +85 -3
  307. package/admin-dist/public/assets/ErrorState-C4nJ-ml4.js +0 -1
  308. package/admin-dist/public/assets/TaxonomyFilter-BgE_SR_O.js +0 -1
  309. package/admin-dist/public/assets/_contentTypeId-DtZectcC.js +0 -1
  310. package/admin-dist/public/assets/content-OEBGlxg1.js +0 -1
  311. package/admin-dist/public/assets/content-types-CjQliqVV.js +0 -2
  312. package/admin-dist/public/assets/globals-hAmgC66w.css +0 -1
  313. package/admin-dist/public/assets/index-BH_ECMhv.js +0 -1
  314. package/admin-dist/public/assets/media-DTJ3-ViE.js +0 -1
  315. package/admin-dist/public/assets/taxonomies-CgG46fIF.js +0 -1
  316. package/admin-dist/public/assets/trash-B3daldm5.js +0 -1
  317. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +0 -89
  318. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-Dd7AmelK.mjs +0 -4
  319. package/dist/client/admin-config.d.ts.map +0 -1
  320. package/dist/client/admin-config.js.map +0 -1
  321. package/dist/client/adminApi.d.ts.map +0 -1
  322. package/dist/client/adminApi.js +0 -736
  323. package/dist/client/adminApi.js.map +0 -1
@@ -0,0 +1,1012 @@
1
+ import { useState, useCallback, useMemo, useEffect } from "react";
2
+ import { useMutation } from "convex/react";
3
+ import { api } from "../../convex/_generated/api";
4
+ import type { FieldType, ContentType } from "convex-cms/types";
5
+ import { CmsDialog } from "~/components/cmsds/CmsDialog";
6
+ import { CmsButton } from "~/components/cmsds/CmsButton";
7
+ import { Input } from "~/components/ui/input";
8
+ import { Label } from "~/components/ui/label";
9
+ import { Textarea } from "~/components/ui/textarea";
10
+ import { Checkbox } from "~/components/ui/checkbox";
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "~/components/ui/select";
18
+ import {
19
+ Plus,
20
+ X,
21
+ ChevronUp,
22
+ ChevronDown,
23
+ AlignLeft,
24
+ FileType,
25
+ Hash,
26
+ ToggleLeft,
27
+ Calendar,
28
+ Link2,
29
+ Image,
30
+ Braces,
31
+ ChevronDownIcon,
32
+ List,
33
+ Tag,
34
+ FolderOpen,
35
+ } from "lucide-react";
36
+ import { cn } from "~/lib/cn";
37
+ import { BreakingChangesWarningDialog } from "./BreakingChangesWarningDialog";
38
+
39
+ interface SelectOption {
40
+ value: string;
41
+ label: string;
42
+ }
43
+
44
+ interface FieldDefinition {
45
+ name: string;
46
+ label: string;
47
+ type: FieldType;
48
+ required: boolean;
49
+ searchable?: boolean;
50
+ localized?: boolean;
51
+ description?: string;
52
+ options?: {
53
+ minLength?: number;
54
+ maxLength?: number;
55
+ pattern?: string;
56
+ min?: number;
57
+ max?: number;
58
+ step?: number;
59
+ precision?: number;
60
+ options?: SelectOption[];
61
+ allowedContentTypes?: string[];
62
+ multiple?: boolean;
63
+ };
64
+ }
65
+
66
+ const FIELD_TYPE_INFO: Record<
67
+ FieldType,
68
+ { label: string; icon: React.ReactNode; description: string }
69
+ > = {
70
+ text: {
71
+ label: "Text",
72
+ icon: <AlignLeft className="size-4" />,
73
+ description: "Single line text input",
74
+ },
75
+ richText: {
76
+ label: "Rich Text",
77
+ icon: <FileType className="size-4" />,
78
+ description: "Multi-line formatted text",
79
+ },
80
+ number: {
81
+ label: "Number",
82
+ icon: <Hash className="size-4" />,
83
+ description: "Numeric value",
84
+ },
85
+ boolean: {
86
+ label: "Boolean",
87
+ icon: <ToggleLeft className="size-4" />,
88
+ description: "True/false toggle",
89
+ },
90
+ date: {
91
+ label: "Date",
92
+ icon: <Calendar className="size-4" />,
93
+ description: "Date picker",
94
+ },
95
+ datetime: {
96
+ label: "Date & Time",
97
+ icon: <Calendar className="size-4" />,
98
+ description: "Date and time picker",
99
+ },
100
+ reference: {
101
+ label: "Reference",
102
+ icon: <Link2 className="size-4" />,
103
+ description: "Link to another content entry",
104
+ },
105
+ media: {
106
+ label: "Media",
107
+ icon: <Image className="size-4" />,
108
+ description: "Image, video, or file",
109
+ },
110
+ json: {
111
+ label: "JSON",
112
+ icon: <Braces className="size-4" />,
113
+ description: "Custom JSON data",
114
+ },
115
+ select: {
116
+ label: "Select",
117
+ icon: <ChevronDownIcon className="size-4" />,
118
+ description: "Dropdown selection",
119
+ },
120
+ multiSelect: {
121
+ label: "Multi-Select",
122
+ icon: <List className="size-4" />,
123
+ description: "Multiple selections",
124
+ },
125
+ tags: {
126
+ label: "Tags",
127
+ icon: <Tag className="size-4" />,
128
+ description: "Free-form tag list",
129
+ },
130
+ category: {
131
+ label: "Category",
132
+ icon: <FolderOpen className="size-4" />,
133
+ description: "Taxonomy category selection",
134
+ },
135
+ };
136
+
137
+ interface ContentTypeFormModalProps {
138
+ isOpen: boolean;
139
+ onClose: () => void;
140
+ onCreated?: (contentType: unknown) => void;
141
+ onUpdated?: (contentType: unknown) => void;
142
+ contentType?: ContentType | null;
143
+ }
144
+
145
+ function generateMachineName(displayName: string): string {
146
+ return displayName
147
+ .toLowerCase()
148
+ .trim()
149
+ .replace(/[^a-z0-9\s]/g, "")
150
+ .replace(/\s+/g, "_")
151
+ .replace(/^[0-9]/, "_$&")
152
+ .slice(0, 64);
153
+ }
154
+
155
+ function isValidMachineName(name: string): boolean {
156
+ return /^[a-z][a-z0-9_]{0,63}$/.test(name);
157
+ }
158
+
159
+ export function ContentTypeFormModal({
160
+ isOpen,
161
+ onClose,
162
+ onCreated,
163
+ onUpdated,
164
+ contentType,
165
+ }: ContentTypeFormModalProps) {
166
+ const isEditing = !!contentType;
167
+
168
+ const [displayName, setDisplayName] = useState("");
169
+ const [machineName, setMachineName] = useState("");
170
+ const [machineNameManuallyEdited, setMachineNameManuallyEdited] = useState(
171
+ false,
172
+ );
173
+ const [description, setDescription] = useState("");
174
+ const [singleton, setSingleton] = useState(false);
175
+ const [fields, setFields] = useState<FieldDefinition[]>([
176
+ { name: "title", label: "Title", type: "text", required: true },
177
+ ]);
178
+ const [titleField, setTitleField] = useState("title");
179
+ const [slugField, setSlugField] = useState("title");
180
+
181
+ const [isSubmitting, setIsSubmitting] = useState(false);
182
+ const [submitError, setSubmitError] = useState<string | null>(null);
183
+ const [activeFieldIndex, setActiveFieldIndex] = useState<number | null>(null);
184
+ const [showFieldEditor, setShowFieldEditor] = useState(false);
185
+
186
+ // Breaking changes state
187
+ const [breakingChanges, setBreakingChanges] = useState<string[]>([]);
188
+ const [showBreakingWarning, setShowBreakingWarning] = useState(false);
189
+ const [isForceUpdating, setIsForceUpdating] = useState(false);
190
+
191
+ const createContentType = useMutation(api.contentTypes.create);
192
+ const updateContentType = useMutation(api.contentTypes.update);
193
+
194
+ // Populate form when editing
195
+ useEffect(() => {
196
+ if (contentType && isOpen) {
197
+ setDisplayName(contentType.displayName);
198
+ setMachineName(contentType.name);
199
+ setMachineNameManuallyEdited(true);
200
+ setDescription(contentType.description || "");
201
+ setSingleton(contentType.singleton || false);
202
+ setFields(contentType.fields as FieldDefinition[]);
203
+ setTitleField(contentType.titleField || "");
204
+ setSlugField(contentType.slugField || "");
205
+ }
206
+ }, [contentType, isOpen]);
207
+
208
+ const resetForm = useCallback(() => {
209
+ setDisplayName("");
210
+ setMachineName("");
211
+ setMachineNameManuallyEdited(false);
212
+ setDescription("");
213
+ setSingleton(false);
214
+ setFields([
215
+ { name: "title", label: "Title", type: "text", required: true },
216
+ ]);
217
+ setTitleField("title");
218
+ setSlugField("title");
219
+ setIsSubmitting(false);
220
+ setSubmitError(null);
221
+ setActiveFieldIndex(null);
222
+ setShowFieldEditor(false);
223
+ setBreakingChanges([]);
224
+ setShowBreakingWarning(false);
225
+ setIsForceUpdating(false);
226
+ }, []);
227
+
228
+ const handleDisplayNameChange = useCallback(
229
+ (value: string) => {
230
+ setDisplayName(value);
231
+ if (!machineNameManuallyEdited) {
232
+ setMachineName(generateMachineName(value));
233
+ }
234
+ },
235
+ [machineNameManuallyEdited],
236
+ );
237
+
238
+ const handleMachineNameChange = useCallback((value: string) => {
239
+ setMachineNameManuallyEdited(true);
240
+ setMachineName(value.toLowerCase().replace(/[^a-z0-9_]/g, ""));
241
+ }, []);
242
+
243
+ const addField = useCallback(() => {
244
+ const newFieldName = `field_${fields.length + 1}`;
245
+ setFields((prev) => [
246
+ ...prev,
247
+ {
248
+ name: newFieldName,
249
+ label: `Field ${prev.length + 1}`,
250
+ type: "text",
251
+ required: false,
252
+ },
253
+ ]);
254
+ setActiveFieldIndex(fields.length);
255
+ setShowFieldEditor(true);
256
+ }, [fields.length]);
257
+
258
+ const removeField = useCallback(
259
+ (index: number) => {
260
+ const fieldToRemove = fields[index];
261
+ setFields((prev) => prev.filter((_, i) => i !== index));
262
+
263
+ if (titleField === fieldToRemove.name) {
264
+ const firstTextField = fields.find(
265
+ (f, i) => i !== index && f.type === "text",
266
+ );
267
+ setTitleField(firstTextField?.name || "");
268
+ }
269
+ if (slugField === fieldToRemove.name) {
270
+ const firstTextField = fields.find(
271
+ (f, i) => i !== index && f.type === "text",
272
+ );
273
+ setSlugField(firstTextField?.name || "");
274
+ }
275
+
276
+ if (activeFieldIndex === index) {
277
+ setActiveFieldIndex(null);
278
+ setShowFieldEditor(false);
279
+ } else if (activeFieldIndex !== null && activeFieldIndex > index) {
280
+ setActiveFieldIndex(activeFieldIndex - 1);
281
+ }
282
+ },
283
+ [fields, activeFieldIndex, titleField, slugField],
284
+ );
285
+
286
+ const updateField = useCallback(
287
+ (index: number, updates: Partial<FieldDefinition>) => {
288
+ setFields((prev) =>
289
+ prev.map((field, i) =>
290
+ i === index ? { ...field, ...updates } : field,
291
+ ),
292
+ );
293
+ },
294
+ [],
295
+ );
296
+
297
+ const moveField = useCallback((fromIndex: number, toIndex: number) => {
298
+ setFields((prev) => {
299
+ const newFields = [...prev];
300
+ const [movedField] = newFields.splice(fromIndex, 1);
301
+ newFields.splice(toIndex, 0, movedField);
302
+ return newFields;
303
+ });
304
+ setActiveFieldIndex(toIndex);
305
+ }, []);
306
+
307
+ const validationErrors = useMemo(() => {
308
+ const errors: string[] = [];
309
+
310
+ if (!displayName.trim()) {
311
+ errors.push("Display name is required");
312
+ }
313
+
314
+ if (!machineName.trim()) {
315
+ errors.push("System Name is required");
316
+ } else if (!isValidMachineName(machineName)) {
317
+ errors.push(
318
+ "System Name must start with a letter and contain only lowercase letters, numbers, and underscores",
319
+ );
320
+ }
321
+
322
+ if (fields.length === 0) {
323
+ errors.push("At least one field is required");
324
+ }
325
+
326
+ const fieldNames = fields.map((f) => f.name);
327
+ const duplicates = fieldNames.filter(
328
+ (name, index) => fieldNames.indexOf(name) !== index,
329
+ );
330
+ if (duplicates.length > 0) {
331
+ errors.push(
332
+ `Duplicate field names: ${[...new Set(duplicates)].join(", ")}`,
333
+ );
334
+ }
335
+
336
+ for (const field of fields) {
337
+ if (!field.name.trim()) {
338
+ errors.push(`Field "${field.label}" has an empty name`);
339
+ } else if (!/^[a-z][a-z0-9_]{0,63}$/.test(field.name)) {
340
+ errors.push(`Field "${field.name}" has an invalid name format`);
341
+ }
342
+ if (!field.label.trim()) {
343
+ errors.push(`Field with name "${field.name}" has an empty label`);
344
+ }
345
+
346
+ if (
347
+ (field.type === "select" || field.type === "multiSelect") &&
348
+ (!field.options?.options || field.options.options.length === 0)
349
+ ) {
350
+ errors.push(
351
+ `${field.type} field "${field.label}" requires at least one option`,
352
+ );
353
+ }
354
+ }
355
+
356
+ return errors;
357
+ }, [displayName, machineName, fields]);
358
+
359
+ const textFields = useMemo(() => fields.filter((f) => f.type === "text"), [
360
+ fields,
361
+ ]);
362
+
363
+ const parseBreakingChanges = (errorMessage: string): string[] => {
364
+ const lines = errorMessage.split("\n");
365
+ return lines
366
+ .filter((line) => line.trim().startsWith("-"))
367
+ .map((line) => line.trim().substring(2));
368
+ };
369
+
370
+ const handleSubmit = useCallback(
371
+ async (e: React.FormEvent, force = false) => {
372
+ e.preventDefault();
373
+
374
+ if (validationErrors.length > 0) {
375
+ setSubmitError(validationErrors.join(". "));
376
+ return;
377
+ }
378
+
379
+ setIsSubmitting(true);
380
+ setSubmitError(null);
381
+
382
+ try {
383
+ if (isEditing && contentType) {
384
+ // Update existing content type
385
+ const result = await updateContentType({
386
+ id: contentType._id,
387
+ displayName: displayName.trim(),
388
+ description: description.trim() || undefined,
389
+ fields: fields as typeof fields,
390
+ singleton,
391
+ titleField: titleField || undefined,
392
+ slugField: slugField || undefined,
393
+ force,
394
+ } as Parameters<typeof updateContentType>[0]);
395
+
396
+ onUpdated?.(result);
397
+ resetForm();
398
+ onClose();
399
+ } else {
400
+ // Create new content type
401
+ const result = await createContentType({
402
+ name: machineName,
403
+ displayName: displayName.trim(),
404
+ description: description.trim() || undefined,
405
+ fields: fields as typeof fields,
406
+ singleton,
407
+ titleField: titleField || undefined,
408
+ slugField: slugField || undefined,
409
+ } as Parameters<typeof createContentType>[0]);
410
+
411
+ onCreated?.(result);
412
+ resetForm();
413
+ onClose();
414
+ }
415
+ } catch (error) {
416
+ const message =
417
+ error instanceof Error
418
+ ? error.message
419
+ : isEditing
420
+ ? "Failed to update content type"
421
+ : "Failed to create content type";
422
+
423
+ // Check for breaking changes error
424
+ if (isEditing && !force && message.includes("breaking change")) {
425
+ const changes = parseBreakingChanges(message);
426
+ setBreakingChanges(changes);
427
+ setShowBreakingWarning(true);
428
+ } else {
429
+ setSubmitError(message);
430
+ }
431
+ } finally {
432
+ setIsSubmitting(false);
433
+ }
434
+ },
435
+ [
436
+ validationErrors,
437
+ isEditing,
438
+ contentType,
439
+ createContentType,
440
+ updateContentType,
441
+ machineName,
442
+ displayName,
443
+ description,
444
+ fields,
445
+ singleton,
446
+ titleField,
447
+ slugField,
448
+ onCreated,
449
+ onUpdated,
450
+ resetForm,
451
+ onClose,
452
+ ],
453
+ );
454
+
455
+ const handleForceUpdate = useCallback(async () => {
456
+ setIsForceUpdating(true);
457
+ try {
458
+ if (contentType) {
459
+ const result = await updateContentType({
460
+ id: contentType._id,
461
+ displayName: displayName.trim(),
462
+ description: description.trim() || undefined,
463
+ fields: fields as typeof fields,
464
+ singleton,
465
+ titleField: titleField || undefined,
466
+ slugField: slugField || undefined,
467
+ force: true,
468
+ } as Parameters<typeof updateContentType>[0]);
469
+
470
+ onUpdated?.(result);
471
+ resetForm();
472
+ setShowBreakingWarning(false);
473
+ onClose();
474
+ }
475
+ } catch (error) {
476
+ const message =
477
+ error instanceof Error
478
+ ? error.message
479
+ : "Failed to update content type";
480
+ setSubmitError(message);
481
+ setShowBreakingWarning(false);
482
+ } finally {
483
+ setIsForceUpdating(false);
484
+ }
485
+ }, [
486
+ contentType,
487
+ updateContentType,
488
+ displayName,
489
+ description,
490
+ fields,
491
+ singleton,
492
+ titleField,
493
+ slugField,
494
+ onUpdated,
495
+ resetForm,
496
+ onClose,
497
+ ]);
498
+
499
+ const handleClose = useCallback(() => {
500
+ if (isSubmitting) return;
501
+ resetForm();
502
+ onClose();
503
+ }, [isSubmitting, resetForm, onClose]);
504
+
505
+ if (!isOpen) return null;
506
+
507
+ const activeField =
508
+ activeFieldIndex !== null ? fields[activeFieldIndex] : null;
509
+
510
+ return (
511
+ <>
512
+ <CmsDialog
513
+ open={isOpen}
514
+ onOpenChange={(open) => !open && handleClose()}
515
+ title={isEditing ? "Edit Content Type" : "Create Content Type"}
516
+ size="2xl"
517
+ footer={
518
+ <>
519
+ <CmsButton
520
+ variant="outline"
521
+ onClick={handleClose}
522
+ disabled={isSubmitting}
523
+ >
524
+ Cancel
525
+ </CmsButton>
526
+ <CmsButton
527
+ variant="primary"
528
+ onClick={handleSubmit}
529
+ disabled={validationErrors.length > 0}
530
+ loading={isSubmitting}
531
+ data-testid={
532
+ isEditing
533
+ ? "update-content-type-submit"
534
+ : "create-content-type-submit"
535
+ }
536
+ >
537
+ {isEditing ? "Save Changes" : "Create Content Type"}
538
+ </CmsButton>
539
+ </>
540
+ }
541
+ >
542
+ <form onSubmit={handleSubmit} className="space-y-6">
543
+ {/* Basic Info Section */}
544
+ <div className="space-y-4">
545
+ <h4 className="text-sm font-semibold text-foreground">
546
+ Basic Information
547
+ </h4>
548
+
549
+ <div className="space-y-2">
550
+ <Label htmlFor="displayName">
551
+ Display Name <span className="text-destructive">*</span>
552
+ </Label>
553
+ <Input
554
+ id="displayName"
555
+ value={displayName}
556
+ onChange={(e) => handleDisplayNameChange(e.target.value)}
557
+ placeholder="e.g., Blog Post"
558
+ disabled={isSubmitting}
559
+ autoFocus
560
+ data-testid="display-name-input"
561
+ />
562
+ </div>
563
+
564
+ <div className="space-y-2">
565
+ <Label htmlFor="machineName">
566
+ System Name <span className="text-destructive">*</span>
567
+ </Label>
568
+ <Input
569
+ id="machineName"
570
+ value={machineName}
571
+ onChange={(e) => handleMachineNameChange(e.target.value)}
572
+ placeholder="e.g., blog_post"
573
+ disabled={isSubmitting || isEditing}
574
+ className={cn(
575
+ !isValidMachineName(machineName) &&
576
+ machineName &&
577
+ "border-destructive",
578
+ )}
579
+ data-testid="machine-name-input"
580
+ />
581
+ <p className="text-xs text-muted-foreground">
582
+ {isEditing
583
+ ? "System name cannot be changed after creation"
584
+ : "Lowercase letters, numbers, and underscores only. Used in API queries."}
585
+ </p>
586
+ </div>
587
+
588
+ <div className="space-y-2">
589
+ <Label htmlFor="description">Description</Label>
590
+ <Textarea
591
+ id="description"
592
+ value={description}
593
+ onChange={(e) => setDescription(e.target.value)}
594
+ placeholder="Optional description of this content type"
595
+ disabled={isSubmitting}
596
+ rows={2}
597
+ />
598
+ </div>
599
+
600
+ <div className="flex items-center gap-2">
601
+ <Checkbox
602
+ id="singleton"
603
+ checked={singleton}
604
+ onCheckedChange={(checked) => setSingleton(checked as boolean)}
605
+ disabled={isSubmitting}
606
+ />
607
+ <Label htmlFor="singleton" className="cursor-pointer">
608
+ Singleton (only one entry allowed)
609
+ </Label>
610
+ </div>
611
+ </div>
612
+
613
+ {/* Fields Section */}
614
+ <div className="space-y-4">
615
+ <div className="flex items-center justify-between">
616
+ <h4 className="text-sm font-semibold text-foreground">Fields</h4>
617
+ <CmsButton
618
+ type="button"
619
+ variant="secondary"
620
+ size="sm"
621
+ onClick={addField}
622
+ disabled={isSubmitting}
623
+ data-testid="add-field-button"
624
+ >
625
+ <Plus className="size-3.5" />
626
+ Add Field
627
+ </CmsButton>
628
+ </div>
629
+
630
+ <div className="space-y-2">
631
+ {fields.map((field, index) => (
632
+ <div
633
+ key={index}
634
+ className={cn(
635
+ "flex cursor-pointer items-center gap-2 rounded-lg border p-2 transition-colors hover:bg-muted/50",
636
+ activeFieldIndex === index && "border-primary bg-primary/5",
637
+ )}
638
+ onClick={() => {
639
+ setActiveFieldIndex(index);
640
+ setShowFieldEditor(true);
641
+ }}
642
+ data-testid={`field-item-${index}`}
643
+ >
644
+ <div className="flex flex-col gap-0.5">
645
+ {index > 0 && (
646
+ <button
647
+ type="button"
648
+ onClick={(e) => {
649
+ e.stopPropagation();
650
+ moveField(index, index - 1);
651
+ }}
652
+ className="rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground"
653
+ >
654
+ <ChevronUp className="size-3" />
655
+ </button>
656
+ )}
657
+ {index < fields.length - 1 && (
658
+ <button
659
+ type="button"
660
+ onClick={(e) => {
661
+ e.stopPropagation();
662
+ moveField(index, index + 1);
663
+ }}
664
+ className="rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground"
665
+ >
666
+ <ChevronDown className="size-3" />
667
+ </button>
668
+ )}
669
+ </div>
670
+
671
+ <div className="flex size-8 items-center justify-center rounded bg-muted text-muted-foreground">
672
+ {FIELD_TYPE_INFO[field.type].icon}
673
+ </div>
674
+
675
+ <div className="min-w-0 flex-1">
676
+ <p className="truncate text-sm font-medium">
677
+ {field.label}
678
+ </p>
679
+ <p className="text-xs text-muted-foreground">
680
+ {FIELD_TYPE_INFO[field.type].label}
681
+ {field.required && " *"}
682
+ </p>
683
+ </div>
684
+
685
+ <button
686
+ type="button"
687
+ onClick={(e) => {
688
+ e.stopPropagation();
689
+ removeField(index);
690
+ }}
691
+ disabled={isSubmitting || fields.length === 1}
692
+ className="rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive disabled:opacity-50"
693
+ >
694
+ <X className="size-4" />
695
+ </button>
696
+ </div>
697
+ ))}
698
+ </div>
699
+
700
+ {/* Field Editor Panel */}
701
+ {showFieldEditor && activeField && activeFieldIndex !== null && (
702
+ <div
703
+ className="rounded-lg border bg-muted/30 p-4"
704
+ data-testid="field-editor"
705
+ >
706
+ <div className="mb-4 flex items-center justify-between">
707
+ <h5 className="font-medium">
708
+ Edit Field: {activeField.label}
709
+ </h5>
710
+ <button
711
+ type="button"
712
+ onClick={() => {
713
+ setShowFieldEditor(false);
714
+ setActiveFieldIndex(null);
715
+ }}
716
+ className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
717
+ >
718
+ <X className="size-4" />
719
+ </button>
720
+ </div>
721
+
722
+ <div className="space-y-4">
723
+ <div className="space-y-2">
724
+ <Label htmlFor="fieldLabel">
725
+ Label <span className="text-destructive">*</span>
726
+ </Label>
727
+ <Input
728
+ id="fieldLabel"
729
+ value={activeField.label}
730
+ onChange={(e) =>
731
+ updateField(activeFieldIndex, {
732
+ label: e.target.value,
733
+ name: machineNameManuallyEdited
734
+ ? activeField.name
735
+ : generateMachineName(e.target.value) ||
736
+ activeField.name,
737
+ })
738
+ }
739
+ disabled={isSubmitting}
740
+ data-testid="field-label-input"
741
+ />
742
+ </div>
743
+
744
+ <div className="space-y-2">
745
+ <Label htmlFor="fieldName">
746
+ Name <span className="text-destructive">*</span>
747
+ </Label>
748
+ <Input
749
+ id="fieldName"
750
+ value={activeField.name}
751
+ onChange={(e) =>
752
+ updateField(activeFieldIndex, {
753
+ name: e.target.value
754
+ .toLowerCase()
755
+ .replace(/[^a-z0-9_]/g, ""),
756
+ })
757
+ }
758
+ disabled={isSubmitting}
759
+ data-testid="field-name-input"
760
+ />
761
+ </div>
762
+
763
+ <div className="space-y-2">
764
+ <Label htmlFor="fieldType">
765
+ Type <span className="text-destructive">*</span>
766
+ </Label>
767
+ <Select
768
+ value={activeField.type}
769
+ onValueChange={(value) =>
770
+ updateField(activeFieldIndex, {
771
+ type: value as FieldType,
772
+ options: undefined,
773
+ })
774
+ }
775
+ disabled={isSubmitting}
776
+ >
777
+ <SelectTrigger data-testid="field-type-select">
778
+ <SelectValue />
779
+ </SelectTrigger>
780
+ <SelectContent>
781
+ {Object.entries(FIELD_TYPE_INFO).map(([type, info]) => (
782
+ <SelectItem key={type} value={type}>
783
+ <div className="flex items-center gap-2">
784
+ {info.icon}
785
+ <span>{info.label}</span>
786
+ <span className="text-muted-foreground">
787
+ - {info.description}
788
+ </span>
789
+ </div>
790
+ </SelectItem>
791
+ ))}
792
+ </SelectContent>
793
+ </Select>
794
+ </div>
795
+
796
+ <div className="flex items-center gap-2">
797
+ <Checkbox
798
+ id="fieldRequired"
799
+ checked={activeField.required}
800
+ onCheckedChange={(checked) =>
801
+ updateField(activeFieldIndex, {
802
+ required: checked as boolean,
803
+ })
804
+ }
805
+ disabled={isSubmitting}
806
+ />
807
+ <Label htmlFor="fieldRequired" className="cursor-pointer">
808
+ Required
809
+ </Label>
810
+ </div>
811
+
812
+ <div className="space-y-2">
813
+ <Label htmlFor="fieldDescription">Help Text</Label>
814
+ <Input
815
+ id="fieldDescription"
816
+ value={activeField.description || ""}
817
+ onChange={(e) =>
818
+ updateField(activeFieldIndex, {
819
+ description: e.target.value || undefined,
820
+ })
821
+ }
822
+ placeholder="Help text shown below the field"
823
+ disabled={isSubmitting}
824
+ />
825
+ </div>
826
+
827
+ {(activeField.type === "select" ||
828
+ activeField.type === "multiSelect") && (
829
+ <SelectOptionsEditor
830
+ options={activeField.options?.options || []}
831
+ onChange={(options) =>
832
+ updateField(activeFieldIndex, {
833
+ options: { ...activeField.options, options },
834
+ })
835
+ }
836
+ disabled={isSubmitting}
837
+ />
838
+ )}
839
+ </div>
840
+ </div>
841
+ )}
842
+ </div>
843
+
844
+ {/* Display Settings */}
845
+ {textFields.length > 0 && (
846
+ <div className="space-y-4">
847
+ <h4 className="text-sm font-semibold text-foreground">
848
+ Display Settings
849
+ </h4>
850
+
851
+ <div className="space-y-2">
852
+ <Label htmlFor="titleField">Title Field</Label>
853
+ <Select
854
+ value={titleField || "none"}
855
+ onValueChange={(v) => setTitleField(v === "none" ? "" : v)}
856
+ disabled={isSubmitting}
857
+ >
858
+ <SelectTrigger>
859
+ <SelectValue placeholder="None" />
860
+ </SelectTrigger>
861
+ <SelectContent>
862
+ <SelectItem value="none">None</SelectItem>
863
+ {textFields.map((field) => (
864
+ <SelectItem key={field.name} value={field.name}>
865
+ {field.label}
866
+ </SelectItem>
867
+ ))}
868
+ </SelectContent>
869
+ </Select>
870
+ <p className="text-xs text-muted-foreground">
871
+ Field to display as the entry title in lists
872
+ </p>
873
+ </div>
874
+
875
+ <div className="space-y-2">
876
+ <Label htmlFor="slugField">Slug Field</Label>
877
+ <Select
878
+ value={slugField || "none"}
879
+ onValueChange={(v) => setSlugField(v === "none" ? "" : v)}
880
+ disabled={isSubmitting}
881
+ >
882
+ <SelectTrigger>
883
+ <SelectValue placeholder="None (auto-generate)" />
884
+ </SelectTrigger>
885
+ <SelectContent>
886
+ <SelectItem value="none">None (auto-generate)</SelectItem>
887
+ {textFields.map((field) => (
888
+ <SelectItem key={field.name} value={field.name}>
889
+ {field.label}
890
+ </SelectItem>
891
+ ))}
892
+ </SelectContent>
893
+ </Select>
894
+ <p className="text-xs text-muted-foreground">
895
+ Field to use for generating URL-friendly slugs
896
+ </p>
897
+ </div>
898
+ </div>
899
+ )}
900
+
901
+ {submitError && (
902
+ <div
903
+ className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800"
904
+ role="alert"
905
+ data-testid="submit-error"
906
+ >
907
+ {submitError}
908
+ </div>
909
+ )}
910
+ </form>
911
+ </CmsDialog>
912
+
913
+ <BreakingChangesWarningDialog
914
+ isOpen={showBreakingWarning}
915
+ onClose={() => setShowBreakingWarning(false)}
916
+ breakingChanges={breakingChanges}
917
+ onForceUpdate={handleForceUpdate}
918
+ onCancel={() => {
919
+ setShowBreakingWarning(false);
920
+ setBreakingChanges([]);
921
+ }}
922
+ isLoading={isForceUpdating}
923
+ />
924
+ </>
925
+ );
926
+ }
927
+
928
+ function SelectOptionsEditor({
929
+ options,
930
+ onChange,
931
+ disabled,
932
+ }: {
933
+ options: SelectOption[];
934
+ onChange: (options: SelectOption[]) => void;
935
+ disabled?: boolean;
936
+ }) {
937
+ const addOption = () => {
938
+ onChange([
939
+ ...options,
940
+ { value: `option_${options.length + 1}`, label: "" },
941
+ ]);
942
+ };
943
+
944
+ const removeOption = (index: number) => {
945
+ onChange(options.filter((_, i) => i !== index));
946
+ };
947
+
948
+ const updateOption = (index: number, updates: Partial<SelectOption>) => {
949
+ onChange(
950
+ options.map((opt, i) => (i === index ? { ...opt, ...updates } : opt)),
951
+ );
952
+ };
953
+
954
+ return (
955
+ <div className="space-y-2">
956
+ <Label>
957
+ Options <span className="text-destructive">*</span>
958
+ </Label>
959
+ <div className="space-y-2">
960
+ {options.map((option, index) => (
961
+ <div key={index} className="flex items-center gap-2">
962
+ <Input
963
+ value={option.label}
964
+ onChange={(e) => {
965
+ const label = e.target.value;
966
+ const value = label
967
+ .toLowerCase()
968
+ .replace(/[^a-z0-9]/g, "_")
969
+ .replace(/^_+|_+$/g, "");
970
+ updateOption(index, { label, value });
971
+ }}
972
+ placeholder="Option label"
973
+ disabled={disabled}
974
+ className="flex-1"
975
+ />
976
+ <Input
977
+ value={option.value}
978
+ onChange={(e) =>
979
+ updateOption(index, {
980
+ value: e.target.value
981
+ .toLowerCase()
982
+ .replace(/[^a-z0-9_]/g, ""),
983
+ })
984
+ }
985
+ placeholder="value"
986
+ disabled={disabled}
987
+ className="w-32"
988
+ />
989
+ <button
990
+ type="button"
991
+ onClick={() => removeOption(index)}
992
+ disabled={disabled}
993
+ className="rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
994
+ >
995
+ <X className="size-4" />
996
+ </button>
997
+ </div>
998
+ ))}
999
+ </div>
1000
+ <CmsButton
1001
+ type="button"
1002
+ variant="secondary"
1003
+ size="sm"
1004
+ onClick={addOption}
1005
+ disabled={disabled}
1006
+ >
1007
+ <Plus className="size-3.5" />
1008
+ Add Option
1009
+ </CmsButton>
1010
+ </div>
1011
+ );
1012
+ }