@byline/ui 0.9.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 (368) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +17 -0
  3. package/dist/admin/components/admin-account/change-password.d.ts +9 -0
  4. package/dist/admin/components/admin-account/change-password.d.ts.map +1 -0
  5. package/dist/admin/components/admin-account/change-password.js +192 -0
  6. package/dist/admin/components/admin-account/change-password.module.js +8 -0
  7. package/dist/admin/components/admin-account/change-password_module.css +27 -0
  8. package/dist/admin/components/admin-account/container.d.ts +30 -0
  9. package/dist/admin/components/admin-account/container.d.ts.map +1 -0
  10. package/dist/admin/components/admin-account/container.js +299 -0
  11. package/dist/admin/components/admin-account/container.module.js +28 -0
  12. package/dist/admin/components/admin-account/container_module.css +106 -0
  13. package/dist/admin/components/admin-account/update.d.ts +9 -0
  14. package/dist/admin/components/admin-account/update.d.ts.map +1 -0
  15. package/dist/admin/components/admin-account/update.js +207 -0
  16. package/dist/admin/components/admin-account/update.module.js +8 -0
  17. package/dist/admin/components/admin-account/update_module.css +27 -0
  18. package/dist/admin/components/admin-permissions/inspector.d.ts +5 -0
  19. package/dist/admin/components/admin-permissions/inspector.d.ts.map +1 -0
  20. package/dist/admin/components/admin-permissions/inspector.js +284 -0
  21. package/dist/admin/components/admin-permissions/inspector.module.js +56 -0
  22. package/dist/admin/components/admin-permissions/inspector_module.css +238 -0
  23. package/dist/admin/components/admin-roles/create.d.ts +8 -0
  24. package/dist/admin/components/admin-roles/create.d.ts.map +1 -0
  25. package/dist/admin/components/admin-roles/create.js +177 -0
  26. package/dist/admin/components/admin-roles/create.module.js +8 -0
  27. package/dist/admin/components/admin-roles/create_module.css +27 -0
  28. package/dist/admin/components/admin-roles/permissions.d.ts +11 -0
  29. package/dist/admin/components/admin-roles/permissions.d.ts.map +1 -0
  30. package/dist/admin/components/admin-roles/permissions.js +303 -0
  31. package/dist/admin/components/admin-roles/permissions.module.js +44 -0
  32. package/dist/admin/components/admin-roles/permissions_module.css +192 -0
  33. package/dist/admin/components/admin-roles/update.d.ts +9 -0
  34. package/dist/admin/components/admin-roles/update.d.ts.map +1 -0
  35. package/dist/admin/components/admin-roles/update.js +166 -0
  36. package/dist/admin/components/admin-roles/update.module.js +8 -0
  37. package/dist/admin/components/admin-roles/update_module.css +27 -0
  38. package/dist/admin/components/admin-users/create.d.ts +9 -0
  39. package/dist/admin/components/admin-users/create.d.ts.map +1 -0
  40. package/dist/admin/components/admin-users/create.js +268 -0
  41. package/dist/admin/components/admin-users/create.module.js +10 -0
  42. package/dist/admin/components/admin-users/create_module.css +45 -0
  43. package/dist/admin/components/admin-users/roles.d.ts +12 -0
  44. package/dist/admin/components/admin-users/roles.d.ts.map +1 -0
  45. package/dist/admin/components/admin-users/roles.js +148 -0
  46. package/dist/admin/components/admin-users/roles.module.js +18 -0
  47. package/dist/admin/components/admin-users/roles_module.css +75 -0
  48. package/dist/admin/components/admin-users/set-password.d.ts +9 -0
  49. package/dist/admin/components/admin-users/set-password.d.ts.map +1 -0
  50. package/dist/admin/components/admin-users/set-password.js +170 -0
  51. package/dist/admin/components/admin-users/set-password.module.js +9 -0
  52. package/dist/admin/components/admin-users/set-password_module.css +31 -0
  53. package/dist/admin/components/admin-users/update.d.ts +9 -0
  54. package/dist/admin/components/admin-users/update.d.ts.map +1 -0
  55. package/dist/admin/components/admin-users/update.js +254 -0
  56. package/dist/admin/components/admin-users/update.module.js +9 -0
  57. package/dist/admin/components/admin-users/update_module.css +34 -0
  58. package/dist/admin/components/auth/sign-in-form.d.ts +14 -0
  59. package/dist/admin/components/auth/sign-in-form.d.ts.map +1 -0
  60. package/dist/admin/components/auth/sign-in-form.js +107 -0
  61. package/dist/admin/components/auth/sign-in-form.module.js +10 -0
  62. package/dist/admin/components/auth/sign-in-form_module.css +35 -0
  63. package/dist/admin/components/collections/diff-modal.d.ts +23 -0
  64. package/dist/admin/components/collections/diff-modal.d.ts.map +1 -0
  65. package/dist/admin/components/collections/diff-modal.js +147 -0
  66. package/dist/admin/components/collections/diff-modal.module.js +14 -0
  67. package/dist/admin/components/collections/diff-modal_module.css +56 -0
  68. package/dist/admin/components/collections/status-badge.d.ts +26 -0
  69. package/dist/admin/components/collections/status-badge.d.ts.map +1 -0
  70. package/dist/admin/components/collections/status-badge.js +35 -0
  71. package/dist/admin/components/collections/status-badge.module.js +7 -0
  72. package/dist/admin/components/collections/status-badge_module.css +20 -0
  73. package/dist/admin/group.d.ts +28 -0
  74. package/dist/admin/group.d.ts.map +1 -0
  75. package/dist/admin/group.js +14 -0
  76. package/dist/admin/group.module.js +6 -0
  77. package/dist/admin/group_module.css +19 -0
  78. package/dist/admin/row.d.ts +26 -0
  79. package/dist/admin/row.d.ts.map +1 -0
  80. package/dist/admin/row.js +8 -0
  81. package/dist/admin/row.module.js +5 -0
  82. package/dist/admin/row_module.css +18 -0
  83. package/dist/admin/tabs.d.ts +33 -0
  84. package/dist/admin/tabs.d.ts.map +1 -0
  85. package/dist/admin/tabs.js +34 -0
  86. package/dist/admin/tabs.module.js +10 -0
  87. package/dist/admin/tabs_module.css +68 -0
  88. package/dist/dnd/draggable-sortable/demo/draggable-list-demo.js +105 -0
  89. package/dist/dnd/draggable-sortable/demo/draggable-list-demo.module.js +12 -0
  90. package/dist/dnd/draggable-sortable/demo/draggable-list-demo_module.css +39 -0
  91. package/dist/dnd/draggable-sortable/draggable-sortable-item/index.d.ts +19 -0
  92. package/dist/dnd/draggable-sortable/draggable-sortable-item/index.d.ts.map +1 -0
  93. package/dist/dnd/draggable-sortable/draggable-sortable-item/index.js +27 -0
  94. package/dist/dnd/draggable-sortable/draggable-sortable-item/types.d.ts +25 -0
  95. package/dist/dnd/draggable-sortable/draggable-sortable-item/types.d.ts.map +1 -0
  96. package/dist/dnd/draggable-sortable/draggable-sortable-item/types.js +1 -0
  97. package/dist/dnd/draggable-sortable/draggable-sortable.d.ts +17 -0
  98. package/dist/dnd/draggable-sortable/draggable-sortable.d.ts.map +1 -0
  99. package/dist/dnd/draggable-sortable/draggable-sortable.js +46 -0
  100. package/dist/dnd/draggable-sortable/index.d.ts +5 -0
  101. package/dist/dnd/draggable-sortable/index.d.ts.map +1 -0
  102. package/dist/dnd/draggable-sortable/index.js +4 -0
  103. package/dist/dnd/draggable-sortable/types.d.ts +26 -0
  104. package/dist/dnd/draggable-sortable/types.d.ts.map +1 -0
  105. package/dist/dnd/draggable-sortable/types.js +1 -0
  106. package/dist/dnd/draggable-sortable/use-draggable-sortable/index.d.ts +16 -0
  107. package/dist/dnd/draggable-sortable/use-draggable-sortable/index.d.ts.map +1 -0
  108. package/dist/dnd/draggable-sortable/use-draggable-sortable/index.js +28 -0
  109. package/dist/dnd/draggable-sortable/use-draggable-sortable/types.d.ts +23 -0
  110. package/dist/dnd/draggable-sortable/use-draggable-sortable/types.d.ts.map +1 -0
  111. package/dist/dnd/draggable-sortable/use-draggable-sortable/types.js +1 -0
  112. package/dist/dnd/draggable-sortable/utils.d.ts +14 -0
  113. package/dist/dnd/draggable-sortable/utils.d.ts.map +1 -0
  114. package/dist/dnd/draggable-sortable/utils.js +10 -0
  115. package/dist/fields/array/array-field.d.ts +15 -0
  116. package/dist/fields/array/array-field.d.ts.map +1 -0
  117. package/dist/fields/array/array-field.js +176 -0
  118. package/dist/fields/array/array-field.module.js +11 -0
  119. package/dist/fields/array/array-field_module.css +32 -0
  120. package/dist/fields/blocks/blocks-field.d.ts +14 -0
  121. package/dist/fields/blocks/blocks-field.d.ts.map +1 -0
  122. package/dist/fields/blocks/blocks-field.js +244 -0
  123. package/dist/fields/blocks/blocks-field.module.js +26 -0
  124. package/dist/fields/blocks/blocks-field_module.css +107 -0
  125. package/dist/fields/checkbox/checkbox-field.d.ts +17 -0
  126. package/dist/fields/checkbox/checkbox-field.d.ts.map +1 -0
  127. package/dist/fields/checkbox/checkbox-field.js +27 -0
  128. package/dist/fields/column-formatter.d.ts +21 -0
  129. package/dist/fields/column-formatter.d.ts.map +1 -0
  130. package/dist/fields/column-formatter.js +15 -0
  131. package/dist/fields/date-time-formatter.d.ts +17 -0
  132. package/dist/fields/date-time-formatter.d.ts.map +1 -0
  133. package/dist/fields/date-time-formatter.js +8 -0
  134. package/dist/fields/datetime/datetime-field.d.ts +17 -0
  135. package/dist/fields/datetime/datetime-field.d.ts.map +1 -0
  136. package/dist/fields/datetime/datetime-field.js +37 -0
  137. package/dist/fields/datetime/datetime-field.module.js +5 -0
  138. package/dist/fields/datetime/datetime-field_module.css +4 -0
  139. package/dist/fields/draggable-context-menu.d.ts +7 -0
  140. package/dist/fields/draggable-context-menu.d.ts.map +1 -0
  141. package/dist/fields/draggable-context-menu.js +83 -0
  142. package/dist/fields/draggable-context-menu.module.js +15 -0
  143. package/dist/fields/draggable-context-menu_module.css +91 -0
  144. package/dist/fields/field-helpers.d.ts +27 -0
  145. package/dist/fields/field-helpers.d.ts.map +1 -0
  146. package/dist/fields/field-helpers.js +48 -0
  147. package/dist/fields/field-renderer.d.ts +31 -0
  148. package/dist/fields/field-renderer.d.ts.map +1 -0
  149. package/dist/fields/field-renderer.js +189 -0
  150. package/dist/fields/field-renderer.module.js +8 -0
  151. package/dist/fields/field-renderer_module.css +11 -0
  152. package/dist/fields/file/file-field.d.ts +18 -0
  153. package/dist/fields/file/file-field.d.ts.map +1 -0
  154. package/dist/fields/file/file-field.js +125 -0
  155. package/dist/fields/file/file-field.module.js +13 -0
  156. package/dist/fields/file/file-field_module.css +64 -0
  157. package/dist/fields/group/group-field.d.ts +16 -0
  158. package/dist/fields/group/group-field.d.ts.map +1 -0
  159. package/dist/fields/group/group-field.js +59 -0
  160. package/dist/fields/group/group-field.module.js +9 -0
  161. package/dist/fields/group/group-field_module.css +27 -0
  162. package/dist/fields/image/image-field.d.ts +20 -0
  163. package/dist/fields/image/image-field.d.ts.map +1 -0
  164. package/dist/fields/image/image-field.js +198 -0
  165. package/dist/fields/image/image-field.module.js +21 -0
  166. package/dist/fields/image/image-field_module.css +96 -0
  167. package/dist/fields/image/image-upload-field.d.ts +22 -0
  168. package/dist/fields/image/image-upload-field.d.ts.map +1 -0
  169. package/dist/fields/image/image-upload-field.js +187 -0
  170. package/dist/fields/image/image-upload-field.module.js +19 -0
  171. package/dist/fields/image/image-upload-field_module.css +92 -0
  172. package/dist/fields/local-date-time.d.ts +28 -0
  173. package/dist/fields/local-date-time.d.ts.map +1 -0
  174. package/dist/fields/local-date-time.js +49 -0
  175. package/dist/fields/locale-badge.d.ts +19 -0
  176. package/dist/fields/locale-badge.d.ts.map +1 -0
  177. package/dist/fields/locale-badge.js +10 -0
  178. package/dist/fields/locale-badge.module.js +5 -0
  179. package/dist/fields/locale-badge_module.css +27 -0
  180. package/dist/fields/numerical/numerical-field.d.ts +19 -0
  181. package/dist/fields/numerical/numerical-field.d.ts.map +1 -0
  182. package/dist/fields/numerical/numerical-field.js +73 -0
  183. package/dist/fields/relation/relation-display.d.ts +41 -0
  184. package/dist/fields/relation/relation-display.d.ts.map +1 -0
  185. package/dist/fields/relation/relation-display.js +58 -0
  186. package/dist/fields/relation/relation-display.module.js +9 -0
  187. package/dist/fields/relation/relation-display_module.css +21 -0
  188. package/dist/fields/relation/relation-field.d.ts +19 -0
  189. package/dist/fields/relation/relation-field.d.ts.map +1 -0
  190. package/dist/fields/relation/relation-field.js +133 -0
  191. package/dist/fields/relation/relation-field.module.js +13 -0
  192. package/dist/fields/relation/relation-field_module.css +62 -0
  193. package/dist/fields/relation/relation-picker.d.ts +50 -0
  194. package/dist/fields/relation/relation-picker.d.ts.map +1 -0
  195. package/dist/fields/relation/relation-picker.js +233 -0
  196. package/dist/fields/relation/relation-picker.module.js +26 -0
  197. package/dist/fields/relation/relation-picker_module.css +124 -0
  198. package/dist/fields/relation/relation-summary.d.ts +32 -0
  199. package/dist/fields/relation/relation-summary.d.ts.map +1 -0
  200. package/dist/fields/relation/relation-summary.js +50 -0
  201. package/dist/fields/relation/relation-summary.module.js +11 -0
  202. package/dist/fields/relation/relation-summary_module.css +37 -0
  203. package/dist/fields/select/select-field.d.ts +17 -0
  204. package/dist/fields/select/select-field.d.ts.map +1 -0
  205. package/dist/fields/select/select-field.js +42 -0
  206. package/dist/fields/select/select-field.module.js +5 -0
  207. package/dist/fields/select/select-field_module.css +4 -0
  208. package/dist/fields/sortable-item.d.ts +16 -0
  209. package/dist/fields/sortable-item.d.ts.map +1 -0
  210. package/dist/fields/sortable-item.js +80 -0
  211. package/dist/fields/sortable-item.module.js +22 -0
  212. package/dist/fields/sortable-item_module.css +124 -0
  213. package/dist/fields/text/text-field.d.ts +21 -0
  214. package/dist/fields/text/text-field.d.ts.map +1 -0
  215. package/dist/fields/text/text-field.js +104 -0
  216. package/dist/fields/text/text-field.module.js +6 -0
  217. package/dist/fields/text/text-field_module.css +5 -0
  218. package/dist/fields/text-area/text-area-field.d.ts +21 -0
  219. package/dist/fields/text-area/text-area-field.d.ts.map +1 -0
  220. package/dist/fields/text-area/text-area-field.js +105 -0
  221. package/dist/fields/text-area/text-area-field.module.js +6 -0
  222. package/dist/fields/text-area/text-area-field_module.css +5 -0
  223. package/dist/fields/use-field-change-handler.d.ts +24 -0
  224. package/dist/fields/use-field-change-handler.d.ts.map +1 -0
  225. package/dist/fields/use-field-change-handler.js +52 -0
  226. package/dist/forms/document-actions.d.ts +14 -0
  227. package/dist/forms/document-actions.d.ts.map +1 -0
  228. package/dist/forms/document-actions.js +153 -0
  229. package/dist/forms/document-actions.module.js +18 -0
  230. package/dist/forms/document-actions_module.css +66 -0
  231. package/dist/forms/form-context.d.ts +78 -0
  232. package/dist/forms/form-context.d.ts.map +1 -0
  233. package/dist/forms/form-context.js +420 -0
  234. package/dist/forms/form-renderer.d.ts +66 -0
  235. package/dist/forms/form-renderer.d.ts.map +1 -0
  236. package/dist/forms/form-renderer.js +555 -0
  237. package/dist/forms/form-renderer.module.js +46 -0
  238. package/dist/forms/form-renderer_module.css +242 -0
  239. package/dist/forms/navigation-guard.d.ts +55 -0
  240. package/dist/forms/navigation-guard.d.ts.map +1 -0
  241. package/dist/forms/navigation-guard.js +22 -0
  242. package/dist/forms/path-widget.d.ts +33 -0
  243. package/dist/forms/path-widget.d.ts.map +1 -0
  244. package/dist/forms/path-widget.js +101 -0
  245. package/dist/forms/path-widget.module.js +8 -0
  246. package/dist/forms/path-widget_module.css +29 -0
  247. package/dist/forms/upload-executor.d.ts +58 -0
  248. package/dist/forms/upload-executor.d.ts.map +1 -0
  249. package/dist/forms/upload-executor.js +92 -0
  250. package/dist/react.d.ts +55 -0
  251. package/dist/react.d.ts.map +1 -0
  252. package/dist/react.js +48 -0
  253. package/dist/services/admin-services-context.d.ts +17 -0
  254. package/dist/services/admin-services-context.d.ts.map +1 -0
  255. package/dist/services/admin-services-context.js +13 -0
  256. package/dist/services/admin-services-types.d.ts +130 -0
  257. package/dist/services/admin-services-types.d.ts.map +1 -0
  258. package/dist/services/admin-services-types.js +1 -0
  259. package/dist/services/field-services-context.d.ts +17 -0
  260. package/dist/services/field-services-context.d.ts.map +1 -0
  261. package/dist/services/field-services-context.js +13 -0
  262. package/dist/services/field-services-types.d.ts +64 -0
  263. package/dist/services/field-services-types.d.ts.map +1 -0
  264. package/dist/services/field-services-types.js +1 -0
  265. package/package.json +133 -0
  266. package/src/admin/components/admin-account/change-password.module.css +40 -0
  267. package/src/admin/components/admin-account/change-password.tsx +232 -0
  268. package/src/admin/components/admin-account/container.module.css +158 -0
  269. package/src/admin/components/admin-account/container.tsx +230 -0
  270. package/src/admin/components/admin-account/update.module.css +40 -0
  271. package/src/admin/components/admin-account/update.tsx +263 -0
  272. package/src/admin/components/admin-permissions/inspector.module.css +326 -0
  273. package/src/admin/components/admin-permissions/inspector.tsx +298 -0
  274. package/src/admin/components/admin-roles/create.module.css +40 -0
  275. package/src/admin/components/admin-roles/create.tsx +218 -0
  276. package/src/admin/components/admin-roles/permissions.module.css +279 -0
  277. package/src/admin/components/admin-roles/permissions.tsx +396 -0
  278. package/src/admin/components/admin-roles/update.module.css +40 -0
  279. package/src/admin/components/admin-roles/update.tsx +218 -0
  280. package/src/admin/components/admin-users/create.module.css +63 -0
  281. package/src/admin/components/admin-users/create.tsx +323 -0
  282. package/src/admin/components/admin-users/roles.module.css +119 -0
  283. package/src/admin/components/admin-users/roles.tsx +172 -0
  284. package/src/admin/components/admin-users/set-password.module.css +46 -0
  285. package/src/admin/components/admin-users/set-password.tsx +199 -0
  286. package/src/admin/components/admin-users/update.module.css +49 -0
  287. package/src/admin/components/admin-users/update.tsx +328 -0
  288. package/src/admin/components/auth/sign-in-form.module.css +53 -0
  289. package/src/admin/components/auth/sign-in-form.tsx +118 -0
  290. package/src/admin/components/collections/diff-modal.module.css +79 -0
  291. package/src/admin/components/collections/diff-modal.tsx +171 -0
  292. package/src/admin/components/collections/status-badge.module.css +31 -0
  293. package/src/admin/components/collections/status-badge.tsx +69 -0
  294. package/src/admin/group.module.css +41 -0
  295. package/src/admin/group.tsx +40 -0
  296. package/src/admin/row.module.css +32 -0
  297. package/src/admin/row.tsx +33 -0
  298. package/src/admin/tabs.module.css +107 -0
  299. package/src/admin/tabs.tsx +74 -0
  300. package/src/declarations.d.ts +4 -0
  301. package/src/dnd/draggable-sortable/demo/draggable-list-demo.module.css +65 -0
  302. package/src/dnd/draggable-sortable/demo/draggable-list-demo.tsx +117 -0
  303. package/src/dnd/draggable-sortable/draggable-sortable-item/index.tsx +54 -0
  304. package/src/dnd/draggable-sortable/draggable-sortable-item/types.ts +30 -0
  305. package/src/dnd/draggable-sortable/draggable-sortable.tsx +86 -0
  306. package/src/dnd/draggable-sortable/index.ts +5 -0
  307. package/src/dnd/draggable-sortable/types.ts +24 -0
  308. package/src/dnd/draggable-sortable/use-draggable-sortable/index.tsx +50 -0
  309. package/src/dnd/draggable-sortable/use-draggable-sortable/types.ts +25 -0
  310. package/src/dnd/draggable-sortable/utils.ts +29 -0
  311. package/src/fields/array/array-field.module.css +48 -0
  312. package/src/fields/array/array-field.tsx +266 -0
  313. package/src/fields/blocks/blocks-field.module.css +148 -0
  314. package/src/fields/blocks/blocks-field.tsx +312 -0
  315. package/src/fields/checkbox/checkbox-field.tsx +53 -0
  316. package/src/fields/column-formatter.tsx +31 -0
  317. package/src/fields/date-time-formatter.tsx +22 -0
  318. package/src/fields/datetime/datetime-field.module.css +13 -0
  319. package/src/fields/datetime/datetime-field.tsx +54 -0
  320. package/src/fields/draggable-context-menu.module.css +127 -0
  321. package/src/fields/draggable-context-menu.tsx +85 -0
  322. package/src/fields/field-helpers.ts +66 -0
  323. package/src/fields/field-renderer.module.css +22 -0
  324. package/src/fields/field-renderer.tsx +255 -0
  325. package/src/fields/file/file-field.module.css +88 -0
  326. package/src/fields/file/file-field.tsx +107 -0
  327. package/src/fields/group/group-field.module.css +43 -0
  328. package/src/fields/group/group-field.tsx +84 -0
  329. package/src/fields/image/image-field.module.css +129 -0
  330. package/src/fields/image/image-field.tsx +212 -0
  331. package/src/fields/image/image-upload-field.module.css +123 -0
  332. package/src/fields/image/image-upload-field.tsx +270 -0
  333. package/src/fields/local-date-time.tsx +88 -0
  334. package/src/fields/locale-badge.module.css +37 -0
  335. package/src/fields/locale-badge.tsx +32 -0
  336. package/src/fields/numerical/numerical-field.tsx +112 -0
  337. package/src/fields/relation/relation-display.module.css +36 -0
  338. package/src/fields/relation/relation-display.tsx +130 -0
  339. package/src/fields/relation/relation-field.module.css +83 -0
  340. package/src/fields/relation/relation-field.tsx +202 -0
  341. package/src/fields/relation/relation-picker.module.css +168 -0
  342. package/src/fields/relation/relation-picker.tsx +325 -0
  343. package/src/fields/relation/relation-summary.module.css +55 -0
  344. package/src/fields/relation/relation-summary.tsx +123 -0
  345. package/src/fields/select/select-field.module.css +13 -0
  346. package/src/fields/select/select-field.tsx +56 -0
  347. package/src/fields/sortable-item.module.css +167 -0
  348. package/src/fields/sortable-item.tsx +101 -0
  349. package/src/fields/text/text-field.module.css +13 -0
  350. package/src/fields/text/text-field.tsx +146 -0
  351. package/src/fields/text-area/text-area-field.module.css +13 -0
  352. package/src/fields/text-area/text-area-field.tsx +147 -0
  353. package/src/fields/use-field-change-handler.ts +112 -0
  354. package/src/forms/document-actions.module.css +94 -0
  355. package/src/forms/document-actions.tsx +149 -0
  356. package/src/forms/form-context.tsx +620 -0
  357. package/src/forms/form-renderer.module.css +318 -0
  358. package/src/forms/form-renderer.tsx +786 -0
  359. package/src/forms/navigation-guard.tsx +98 -0
  360. package/src/forms/path-widget.module.css +41 -0
  361. package/src/forms/path-widget.test.tsx +217 -0
  362. package/src/forms/path-widget.tsx +141 -0
  363. package/src/forms/upload-executor.ts +190 -0
  364. package/src/react.ts +79 -0
  365. package/src/services/admin-services-context.tsx +35 -0
  366. package/src/services/admin-services-types.ts +177 -0
  367. package/src/services/field-services-context.tsx +35 -0
  368. package/src/services/field-services-types.ts +68 -0
@@ -0,0 +1,786 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
10
+
11
+ import type {
12
+ CollectionAdminConfig,
13
+ Field,
14
+ GroupDefinition,
15
+ RowDefinition,
16
+ TabSetDefinition,
17
+ WorkflowStatus,
18
+ } from '@byline/core'
19
+ import { Button, ComboButton, Modal } from '@infonomic/uikit/react'
20
+ import cx from 'classnames'
21
+
22
+ import { Group } from '../admin/group'
23
+ import { Row } from '../admin/row'
24
+ import { Tabs } from '../admin/tabs'
25
+ import { FieldRenderer } from '../fields/field-renderer'
26
+ import { LocalDateTime } from '../fields/local-date-time'
27
+ import { useBylineFieldServices } from '../services/field-services-context'
28
+ import { DocumentActions } from './document-actions'
29
+ import { FormProvider, useFieldValue, useFormContext } from './form-context'
30
+ import styles from './form-renderer.module.css'
31
+ import { useNavigationGuardAdapter } from './navigation-guard'
32
+ import { PathWidget } from './path-widget'
33
+ import { executeUploads } from './upload-executor'
34
+ import type { UseNavigationGuard } from './navigation-guard'
35
+
36
+ /** Metadata about a previously published version that is still live. */
37
+ export interface PublishedVersionInfo {
38
+ id: string
39
+ versionId: string
40
+ status: string
41
+ createdAt: string | Date
42
+ updatedAt: string | Date
43
+ }
44
+
45
+ /** Props shared by both the public FormRenderer and its internal FormContent component. */
46
+ export interface FormRendererProps {
47
+ mode: 'create' | 'edit'
48
+ fields: Field[]
49
+ onSubmit: (data: any) => void
50
+ onCancel: () => void
51
+ onStatusChange?: (nextStatus: string) => Promise<void>
52
+ onUnpublish?: () => Promise<void>
53
+ onDelete?: () => Promise<void>
54
+ nextStatus?: WorkflowStatus
55
+ workflowStatuses?: WorkflowStatus[]
56
+ publishedVersion?: PublishedVersionInfo | null
57
+ initialData?: Record<string, any>
58
+ adminConfig?: CollectionAdminConfig
59
+ /**
60
+ * Name of the schema field to render as the live form heading.
61
+ * Sourced from `CollectionDefinition.useAsTitle` by the caller.
62
+ */
63
+ useAsTitle?: string
64
+ /**
65
+ * Name of the schema field that initialises the system path.
66
+ * Sourced from `CollectionDefinition.useAsPath` by the caller. When
67
+ * present the path widget renders in the sidebar.
68
+ */
69
+ useAsPath?: string
70
+ headingLabel?: string
71
+ headerSlot?: ReactNode
72
+ /** Collection path forwarded to upload-capable fields (e.g. `'media'`). */
73
+ collectionPath?: string
74
+ /** The active content locale — initialised from the route query string. */
75
+ initialLocale?: string
76
+ /** Called when the user picks a different content locale. */
77
+ onLocaleChange?: (locale: string) => void
78
+ /**
79
+ * Default content locale used when no `initialLocale` is supplied and as the
80
+ * fallback inside `PathWidget`. Hosts typically pass their app-wide
81
+ * `i18n.content.defaultLocale`. Defaults to `'en'`.
82
+ */
83
+ defaultLocale?: string
84
+ /**
85
+ * Framework-specific navigation guard hook.
86
+ * When provided, this overrides the adapter from `NavigationGuardProvider` context.
87
+ * If neither is set, a no-op `beforeunload`-only guard is used.
88
+ */
89
+ useNavigationGuard?: UseNavigationGuard
90
+ }
91
+
92
+ const FormStatusDisplay = ({
93
+ initialData,
94
+ workflowStatuses,
95
+ publishedVersion,
96
+ onUnpublish,
97
+ }: {
98
+ initialData?: Record<string, any>
99
+ workflowStatuses?: WorkflowStatus[]
100
+ publishedVersion?: PublishedVersionInfo | null
101
+ onUnpublish?: () => Promise<void>
102
+ }) => {
103
+ const statusCode = initialData?.status
104
+ const statusLabel = workflowStatuses?.find((s) => s.name === statusCode)?.label ?? statusCode
105
+ // Single-status workflows (e.g. lookups) have no editorial lifecycle —
106
+ // suppress the "Status: …" cell since there is nothing meaningful to convey.
107
+ const showStatusCell = (workflowStatuses?.length ?? 0) > 1
108
+
109
+ return (
110
+ <div className={cx('byline-form-status', styles.status)}>
111
+ <div className={cx('byline-form-status-meta', styles['status-meta'])}>
112
+ {showStatusCell && (
113
+ <div className={cx('byline-form-status-cell', styles['status-cell'])}>
114
+ <span className={cx('byline-form-status-muted', styles['status-muted'])}>Status:</span>
115
+ <span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
116
+ {statusLabel}
117
+ </span>
118
+ </div>
119
+ )}
120
+
121
+ {initialData?.updatedAt != null && (
122
+ <div className={cx('byline-form-status-cell', styles['status-cell'])}>
123
+ <span className={cx('byline-form-status-muted', styles['status-muted'])}>
124
+ Last modified:
125
+ </span>
126
+ <span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
127
+ <LocalDateTime value={initialData.updatedAt} />
128
+ </span>
129
+ </div>
130
+ )}
131
+
132
+ {initialData?.createdAt != null && (
133
+ <div className={cx('byline-form-status-cell', styles['status-cell'])}>
134
+ <span className={cx('byline-form-status-muted', styles['status-muted'])}>Created:</span>
135
+ <span className={cx('byline-form-status-trunc', styles['status-trunc'])}>
136
+ <LocalDateTime value={initialData.createdAt} />
137
+ </span>
138
+ </div>
139
+ )}
140
+ </div>
141
+
142
+ {publishedVersion != null && (
143
+ <div className={cx('byline-form-status-published', styles['status-published'])}>
144
+ <span className={cx('byline-form-status-muted', styles['status-muted'])}>
145
+ A published version is currently live.{' '}
146
+ {publishedVersion.updatedAt ? (
147
+ <span>
148
+ Published on <LocalDateTime value={publishedVersion.updatedAt} />
149
+ </span>
150
+ ) : (
151
+ ''
152
+ )}
153
+ </span>
154
+ {onUnpublish && (
155
+ <>
156
+ {' '}
157
+ <button
158
+ type="button"
159
+ onClick={onUnpublish}
160
+ className={cx('byline-form-status-unpublish', styles['status-unpublish'])}
161
+ >
162
+ Unpublish
163
+ </button>
164
+ </>
165
+ )}
166
+ </div>
167
+ )}
168
+ </div>
169
+ )
170
+ }
171
+
172
+ /**
173
+ * Compute the primary and secondary status transitions for the ComboButton.
174
+ * - Primary: the main action (forward step, or back step if at the end)
175
+ * - Secondary: other available transitions to show as dropdown options
176
+ */
177
+ function computeStatusTransitions(
178
+ currentStatus: string | undefined,
179
+ workflowStatuses: WorkflowStatus[] | undefined,
180
+ nextStatus: WorkflowStatus | undefined
181
+ ): {
182
+ primaryStatus: WorkflowStatus | undefined
183
+ secondaryStatuses: WorkflowStatus[]
184
+ } {
185
+ if (!workflowStatuses || workflowStatuses.length === 0 || !currentStatus) {
186
+ return { primaryStatus: nextStatus, secondaryStatuses: [] }
187
+ }
188
+
189
+ // Single-status workflows (e.g. SINGLE_STATUS_WORKFLOW for lookups) have
190
+ // no transitions — short-circuit so the form shows only Close / Save.
191
+ if (workflowStatuses.length <= 1) {
192
+ return { primaryStatus: undefined, secondaryStatuses: [] }
193
+ }
194
+
195
+ const currentIndex = workflowStatuses.findIndex((s) => s.name === currentStatus)
196
+ if (currentIndex === -1) {
197
+ return { primaryStatus: nextStatus, secondaryStatuses: [] }
198
+ }
199
+
200
+ const isAtEnd = currentIndex === workflowStatuses.length - 1
201
+ const isAtStart = currentIndex === 0
202
+
203
+ // Collect all available target statuses
204
+ const availableTargets: WorkflowStatus[] = []
205
+
206
+ // Reset to first (if not at first)
207
+ if (!isAtStart && workflowStatuses[0]) {
208
+ availableTargets.push(workflowStatuses[0])
209
+ }
210
+
211
+ // Back one step (if not at start and the previous is not already the first)
212
+ if (currentIndex > 1 && workflowStatuses[currentIndex - 1]) {
213
+ availableTargets.push(workflowStatuses[currentIndex - 1])
214
+ }
215
+
216
+ // Forward one step (if not at end) - this is the nextStatus
217
+ if (!isAtEnd && workflowStatuses[currentIndex + 1]) {
218
+ availableTargets.push(workflowStatuses[currentIndex + 1])
219
+ }
220
+
221
+ // Determine primary and secondary
222
+ let primaryStatus: WorkflowStatus | undefined
223
+ let secondaryStatuses: WorkflowStatus[]
224
+
225
+ if (isAtEnd) {
226
+ // At the last status: primary is the back step (previous status)
227
+ const prevStatus = workflowStatuses[currentIndex - 1]
228
+ primaryStatus = prevStatus
229
+ secondaryStatuses = availableTargets.filter((s) => s.name !== prevStatus?.name)
230
+ } else {
231
+ // Not at end: primary is the forward step (nextStatus)
232
+ primaryStatus = nextStatus
233
+ secondaryStatuses = availableTargets.filter((s) => s.name !== nextStatus?.name)
234
+ }
235
+
236
+ return { primaryStatus, secondaryStatuses }
237
+ }
238
+
239
+ const FormContent = ({
240
+ mode,
241
+ fields,
242
+ onSubmit,
243
+ onCancel,
244
+ onStatusChange,
245
+ onUnpublish,
246
+ onDelete,
247
+ nextStatus,
248
+ workflowStatuses,
249
+ publishedVersion,
250
+ initialData,
251
+ adminConfig,
252
+ useAsTitle,
253
+ useAsPath,
254
+ headingLabel,
255
+ headerSlot,
256
+ collectionPath,
257
+ initialLocale,
258
+ onLocaleChange,
259
+ defaultLocale = 'en',
260
+ useNavigationGuard: useNavigationGuardProp,
261
+ _activeTabBySet,
262
+ _onTabChange,
263
+ }: FormRendererProps & {
264
+ /** Lifted active-tab-per-set map from FormRenderer — preserves tab choices across locale-change remounts. */
265
+ _activeTabBySet?: Record<string, string>
266
+ _onTabChange?: (tabSetName: string, tabName: string) => void
267
+ }) => {
268
+ const {
269
+ getFieldValues,
270
+ runFieldHooks,
271
+ validateForm,
272
+ errors: initialErrors,
273
+ hasChanges: hasChangesFn,
274
+ resetHasChanges,
275
+ getPatches,
276
+ getSystemPath,
277
+ subscribeErrors,
278
+ subscribeMeta,
279
+ setFieldValue,
280
+ setFieldError,
281
+ getPendingUploads,
282
+ clearPendingUploads,
283
+ } = useFormContext()
284
+
285
+ const [errors, setErrors] = useState(initialErrors)
286
+ const [hasChanges, setHasChanges] = useState(hasChangesFn())
287
+ const [statusBusy, setStatusBusy] = useState(false)
288
+ const [isUploading, setIsUploading] = useState(false)
289
+ const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale)
290
+ const { uploadField } = useBylineFieldServices()
291
+
292
+ // Sync contentLocale when the route re-fetches with a different locale.
293
+ useEffect(() => {
294
+ if (initialLocale) setContentLocale(initialLocale)
295
+ }, [initialLocale])
296
+
297
+ // ---------------------------------------------------------------------
298
+ // Layout primitives + lookup tables.
299
+ //
300
+ // Built once per render from `adminConfig`. The validator at startup
301
+ // guarantees every reachable name resolves and every schema field is
302
+ // placed at most once, so render-time lookups are unguarded.
303
+ // ---------------------------------------------------------------------
304
+
305
+ const fieldByName = useMemo(() => {
306
+ const map = new Map<string, Field>()
307
+ for (const field of fields) {
308
+ if ('name' in field) map.set(field.name, field)
309
+ }
310
+ return map
311
+ }, [fields])
312
+
313
+ const tabSetByName = useMemo(() => {
314
+ const map = new Map<string, TabSetDefinition>()
315
+ for (const set of adminConfig?.tabSets ?? []) map.set(set.name, set)
316
+ return map
317
+ }, [adminConfig])
318
+
319
+ const rowByName = useMemo(() => {
320
+ const map = new Map<string, RowDefinition>()
321
+ for (const row of adminConfig?.rows ?? []) map.set(row.name, row)
322
+ return map
323
+ }, [adminConfig])
324
+
325
+ const groupByName = useMemo(() => {
326
+ const map = new Map<string, GroupDefinition>()
327
+ for (const group of adminConfig?.groups ?? []) map.set(group.name, group)
328
+ return map
329
+ }, [adminConfig])
330
+
331
+ // When `layout` is omitted, synthesise main = all schema fields in order.
332
+ const layout = useMemo(() => {
333
+ if (adminConfig?.layout) return adminConfig.layout
334
+ return { main: fields.filter((f) => 'name' in f).map((f) => (f as { name: string }).name) }
335
+ }, [adminConfig, fields])
336
+
337
+ // Reverse index: schema field name → which tab set + tab it lives in.
338
+ // Powers per-tab-set error badge counts. Fields not under any tab set
339
+ // (e.g. raw-field placement directly in `layout.main`) are absent from
340
+ // this map.
341
+ const fieldToTabPath = useMemo(() => {
342
+ const map = new Map<string, { tabSetName: string; tabName: string }>()
343
+ const visit = (
344
+ names: readonly string[],
345
+ tabSetName: string,
346
+ tabName: string,
347
+ seen: Set<string>
348
+ ) => {
349
+ for (const name of names) {
350
+ if (fieldByName.has(name)) {
351
+ map.set(name, { tabSetName, tabName })
352
+ } else if (seen.has(name)) {
353
+ } else if (rowByName.has(name)) {
354
+ const row = rowByName.get(name)!
355
+ const next = new Set(seen).add(name)
356
+ visit(row.fields, tabSetName, tabName, next)
357
+ } else if (groupByName.has(name)) {
358
+ const group = groupByName.get(name)!
359
+ const next = new Set(seen).add(name)
360
+ visit(group.fields, tabSetName, tabName, next)
361
+ }
362
+ }
363
+ }
364
+ for (const set of adminConfig?.tabSets ?? []) {
365
+ for (const tab of set.tabs) {
366
+ visit(tab.fields, set.name, tab.name, new Set())
367
+ }
368
+ }
369
+ return map
370
+ }, [adminConfig, fieldByName, rowByName, groupByName])
371
+
372
+ // ---------------------------------------------------------------------
373
+ // Active-tab state — one tab name per declared tab set.
374
+ // Lifted into FormRenderer via `_activeTabBySet` / `_onTabChange` so the
375
+ // user's tab choices survive the locale-change remount triggered by
376
+ // FormProvider's `key` prop.
377
+ // ---------------------------------------------------------------------
378
+
379
+ const tabSets = adminConfig?.tabSets ?? []
380
+
381
+ const initialActiveTabBySet = useMemo<Record<string, string>>(() => {
382
+ const result: Record<string, string> = {}
383
+ for (const set of tabSets) {
384
+ const saved = _activeTabBySet?.[set.name]
385
+ if (saved && set.tabs.some((t) => t.name === saved)) {
386
+ result[set.name] = saved
387
+ } else {
388
+ result[set.name] = set.tabs[0]?.name ?? ''
389
+ }
390
+ }
391
+ return result
392
+ // initial-only; subsequent updates flow through setActiveTabBySet.
393
+ // eslint-disable-next-line react-hooks/exhaustive-deps
394
+ }, [tabSets, _activeTabBySet])
395
+
396
+ const [activeTabBySet, setActiveTabBySet] =
397
+ useState<Record<string, string>>(initialActiveTabBySet)
398
+
399
+ const handleTabChange = useCallback(
400
+ (tabSetName: string, tabName: string) => {
401
+ setActiveTabBySet((prev) => ({ ...prev, [tabSetName]: tabName }))
402
+ _onTabChange?.(tabSetName, tabName)
403
+ },
404
+ [_onTabChange]
405
+ )
406
+
407
+ // Track live form data so TabDefinition.condition functions can react to
408
+ // field changes. Re-evaluated per keystroke via the meta-subscribe loop.
409
+ const [formData, setFormData] = useState<Record<string, any>>(() => getFieldValues())
410
+
411
+ // Live document heading — tracks the useAsTitle field as the user types
412
+ const liveTitle = useFieldValue<string>(useAsTitle ?? '')
413
+ const heading =
414
+ liveTitle ||
415
+ (headingLabel
416
+ ? `${mode === 'create' ? 'Create' : 'Edit'} ${headingLabel}`
417
+ : mode === 'create'
418
+ ? 'Create'
419
+ : 'Edit')
420
+
421
+ // Navigation guard — block router navigation and browser unload when dirty.
422
+ // The guard hook is injected by the consuming framework (prop > context > no-op fallback).
423
+ const guardFromContext = useNavigationGuardAdapter()
424
+ const useGuard = useNavigationGuardProp ?? guardFromContext
425
+ const guard = useGuard(hasChanges)
426
+
427
+ // Compute available status transitions
428
+ const currentStatus = initialData?.status
429
+ const { primaryStatus, secondaryStatuses } = computeStatusTransitions(
430
+ currentStatus,
431
+ workflowStatuses,
432
+ nextStatus
433
+ )
434
+
435
+ useEffect(() => {
436
+ return subscribeErrors((newErrors) => setErrors(newErrors))
437
+ }, [subscribeErrors])
438
+
439
+ useEffect(() => {
440
+ return subscribeMeta(() => setHasChanges(hasChangesFn()))
441
+ }, [subscribeMeta, hasChangesFn])
442
+
443
+ // Keep formData in sync for evaluating TabDefinition.condition functions
444
+ useEffect(() => {
445
+ return subscribeMeta(() => setFormData(getFieldValues()))
446
+ }, [subscribeMeta, getFieldValues])
447
+
448
+ const handleCancel = () => {
449
+ if (onCancel && typeof onCancel === 'function') {
450
+ onCancel()
451
+ }
452
+ }
453
+
454
+ const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
455
+ e.preventDefault()
456
+
457
+ // Run field-level beforeValidate hooks (submit-time), then validate
458
+ void (async () => {
459
+ const hookErrors = await runFieldHooks(fields)
460
+ const formErrors = validateForm(fields)
461
+ const allErrors = [...hookErrors, ...formErrors]
462
+
463
+ if (allErrors.length > 0) {
464
+ console.error('Form validation failed:', allErrors)
465
+ return
466
+ }
467
+
468
+ // Execute any pending uploads before submitting
469
+ const pendingUploads = getPendingUploads()
470
+ if (pendingUploads.size > 0) {
471
+ setIsUploading(true)
472
+ try {
473
+ const uploadResult = await executeUploads(pendingUploads, uploadField)
474
+
475
+ // Check for upload errors
476
+ if (!uploadResult.allSucceeded) {
477
+ // Set field-level errors for failed uploads
478
+ for (const [fieldPath, errorMessage] of uploadResult.errors.entries()) {
479
+ setFieldError(fieldPath, `Upload failed: ${errorMessage}`)
480
+ }
481
+ console.error('One or more uploads failed:', uploadResult.errors)
482
+ setIsUploading(false)
483
+ return
484
+ }
485
+
486
+ // Replace pending StoredFileValues with real ones in form data
487
+ for (const [fieldPath, storedFile] of uploadResult.successful.entries()) {
488
+ setFieldValue(fieldPath, storedFile)
489
+ }
490
+
491
+ // Clear pending uploads (blob URLs already revoked by clearPendingUploads)
492
+ clearPendingUploads()
493
+ } catch (err) {
494
+ console.error('Upload execution error:', err)
495
+ setIsUploading(false)
496
+ return
497
+ }
498
+ setIsUploading(false)
499
+ }
500
+
501
+ const data = getFieldValues()
502
+ const patches = getPatches()
503
+ const systemPath = getSystemPath()
504
+
505
+ if (onSubmit && typeof onSubmit === 'function') {
506
+ onSubmit({ data, patches, systemPath })
507
+ resetHasChanges()
508
+ }
509
+ })()
510
+ }
511
+
512
+ // Per-tab-set error counts: { [tabSetName]: { [tabName]: count } }.
513
+ // Each <Tabs> bar consumes its own slice.
514
+ const tabErrorCountsBySet = useMemo<Record<string, Record<string, number>>>(() => {
515
+ const result: Record<string, Record<string, number>> = {}
516
+ for (const err of errors) {
517
+ const path = fieldToTabPath.get(err.field)
518
+ if (!path) continue
519
+ result[path.tabSetName] ??= {}
520
+ result[path.tabSetName]![path.tabName] = (result[path.tabSetName]?.[path.tabName] ?? 0) + 1
521
+ }
522
+ return result
523
+ }, [errors, fieldToTabPath])
524
+
525
+ // -------------------------------------------------------------------
526
+ // Layout walk — recursively dispatches each name in a region to the
527
+ // appropriate primitive renderer or to <FieldRenderer>.
528
+ // -------------------------------------------------------------------
529
+
530
+ const renderField = (fieldName: string): ReactNode => {
531
+ const field = fieldByName.get(fieldName)
532
+ if (!field) return null
533
+ return (
534
+ <FieldRenderer
535
+ key={field.name}
536
+ field={field}
537
+ defaultValue={initialData?.fields?.[field.name]}
538
+ collectionPath={collectionPath}
539
+ contentLocale={contentLocale}
540
+ components={adminConfig?.fields?.[field.name]?.components}
541
+ />
542
+ )
543
+ }
544
+
545
+ const renderItem = (name: string): ReactNode => {
546
+ const tabSet = tabSetByName.get(name)
547
+ if (tabSet) return renderTabSet(tabSet)
548
+
549
+ const group = groupByName.get(name)
550
+ if (group) return renderGroup(group)
551
+
552
+ const row = rowByName.get(name)
553
+ if (row) return renderRow(row)
554
+
555
+ return renderField(name)
556
+ }
557
+
558
+ const renderRow = (row: RowDefinition): ReactNode => (
559
+ <Row key={`row:${row.name}`}>{row.fields.map((name) => renderField(name))}</Row>
560
+ )
561
+
562
+ const renderGroup = (group: GroupDefinition): ReactNode => (
563
+ <Group key={`group:${group.name}`} label={group.label}>
564
+ {group.fields.map((name) => renderItem(name))}
565
+ </Group>
566
+ )
567
+
568
+ const renderTabSet = (set: TabSetDefinition): ReactNode => {
569
+ const visibleTabs = set.tabs.filter((tab) => !tab.condition || tab.condition(formData))
570
+ const requested = activeTabBySet[set.name] ?? ''
571
+ const resolvedActive =
572
+ visibleTabs.length > 0 && !visibleTabs.some((t) => t.name === requested)
573
+ ? (visibleTabs[0]?.name ?? requested)
574
+ : requested
575
+ const activeTab = visibleTabs.find((t) => t.name === resolvedActive)
576
+
577
+ return (
578
+ <div key={`tabset:${set.name}`} className={cx('byline-form-tabset', styles.tabset)}>
579
+ {visibleTabs.length > 0 && (
580
+ <Tabs
581
+ tabs={visibleTabs}
582
+ activeTab={resolvedActive}
583
+ onChange={(tabName) => handleTabChange(set.name, tabName)}
584
+ errorCounts={tabErrorCountsBySet[set.name]}
585
+ className={cx('byline-form-tabset-tabs', styles['tabset-tabs'])}
586
+ />
587
+ )}
588
+ {activeTab && (
589
+ <div className={cx('byline-form-tabset-fields', styles['tabset-fields'])}>
590
+ {activeTab.fields.map((name) => renderItem(name))}
591
+ </div>
592
+ )}
593
+ </div>
594
+ )
595
+ }
596
+
597
+ return (
598
+ <form noValidate onSubmit={handleSubmit} className={cx('byline-form', styles.form)}>
599
+ <div className={cx('byline-form-heading-row', styles['heading-row'])}>
600
+ <h1 className={cx('byline-form-heading', styles.heading)}>{heading}</h1>
601
+ {headerSlot}
602
+ </div>
603
+ <div className={cx('byline-form-status-bar', styles['status-bar'])}>
604
+ <FormStatusDisplay
605
+ initialData={initialData}
606
+ workflowStatuses={workflowStatuses}
607
+ publishedVersion={publishedVersion}
608
+ onUnpublish={onUnpublish}
609
+ />
610
+ <div className={cx('byline-form-actions', styles.actions)}>
611
+ <Button
612
+ className={cx('byline-form-actions-button', styles['actions-button'])}
613
+ size="sm"
614
+ intent="noeffect"
615
+ type="button"
616
+ onClick={handleCancel}
617
+ >
618
+ {hasChanges === false ? 'Close' : 'Cancel'}
619
+ </Button>
620
+ <Button
621
+ className={cx('byline-form-actions-button', styles['actions-button'])}
622
+ size="sm"
623
+ type="submit"
624
+ disabled={hasChanges === false || isUploading}
625
+ >
626
+ {isUploading ? 'Uploading…' : 'Save'}
627
+ </Button>
628
+ {primaryStatus && onStatusChange && (
629
+ <div className={cx('byline-form-actions-status-wrap', styles['actions-status-wrap'])}>
630
+ <ComboButton
631
+ buttonClassName={cx(
632
+ 'byline-form-actions-combo-button',
633
+ styles['actions-combo-button']
634
+ )}
635
+ triggerClassName={cx(
636
+ 'byline-form-actions-combo-trigger',
637
+ styles['actions-combo-trigger']
638
+ )}
639
+ options={secondaryStatuses.map((s) => ({
640
+ label: s.verb ?? s.label ?? s.name,
641
+ value: s.name,
642
+ }))}
643
+ sideOffset={5}
644
+ size="sm"
645
+ type="button"
646
+ intent="success"
647
+ disabled={statusBusy}
648
+ onOptionSelect={async (value: string) => {
649
+ setStatusBusy(true)
650
+ try {
651
+ await onStatusChange(value)
652
+ } finally {
653
+ setStatusBusy(false)
654
+ }
655
+ }}
656
+ onButtonClick={async () => {
657
+ setStatusBusy(true)
658
+ try {
659
+ await onStatusChange(primaryStatus.name)
660
+ } finally {
661
+ setStatusBusy(false)
662
+ }
663
+ }}
664
+ >
665
+ {statusBusy
666
+ ? '...'
667
+ : (primaryStatus.verb ?? primaryStatus.label ?? primaryStatus.name)}
668
+ </ComboButton>
669
+ </div>
670
+ )}
671
+ <DocumentActions
672
+ publishedVersion={publishedVersion}
673
+ onUnpublish={onUnpublish}
674
+ onDelete={onDelete}
675
+ />
676
+ </div>
677
+ </div>
678
+ <div className={cx('byline-form-layout', styles.layout)}>
679
+ <div className={cx('byline-form-content', styles.content)}>
680
+ {layout.main.map((name) => renderItem(name))}
681
+ </div>
682
+ <div className={cx('byline-form-sidebar', styles.sidebar)}>
683
+ {(useAsPath ||
684
+ (typeof initialData?.path === 'string' && initialData.path.length > 0)) && (
685
+ <PathWidget
686
+ useAsPath={useAsPath}
687
+ collectionPath={collectionPath ?? ''}
688
+ defaultLocale={defaultLocale}
689
+ mode={mode}
690
+ />
691
+ )}
692
+ {(layout.sidebar ?? []).map((name) => renderItem(name))}
693
+ </div>
694
+ </div>
695
+ {guard.isBlocked && (
696
+ <Modal isOpen={true} closeOnOverlayClick={false} onDismiss={guard.stay}>
697
+ <Modal.Container style={{ maxWidth: '460px' }}>
698
+ <Modal.Header
699
+ className={cx('byline-form-guard-modal-head', styles['guard-modal-head'])}
700
+ >
701
+ <h3 className={cx('byline-form-guard-modal-title', styles['guard-modal-title'])}>
702
+ Leave without saving?
703
+ </h3>
704
+ </Modal.Header>
705
+ <Modal.Content>
706
+ <p className={cx('byline-form-guard-modal-text', styles['guard-modal-text'])}>
707
+ Your changes have not been saved. If you leave now, you will lose your changes.
708
+ </p>
709
+ </Modal.Content>
710
+ <Modal.Actions>
711
+ <Button size="sm" intent="noeffect" type="button" onClick={guard.stay}>
712
+ Stay on this page
713
+ </Button>
714
+ <Button size="sm" intent="danger" type="button" onClick={guard.proceed}>
715
+ Leave anyway
716
+ </Button>
717
+ </Modal.Actions>
718
+ </Modal.Container>
719
+ </Modal>
720
+ )}
721
+ </form>
722
+ )
723
+ }
724
+
725
+ export const FormRenderer = ({
726
+ mode,
727
+ fields,
728
+ onSubmit,
729
+ onCancel,
730
+ onStatusChange,
731
+ onUnpublish,
732
+ onDelete,
733
+ nextStatus,
734
+ workflowStatuses,
735
+ publishedVersion,
736
+ initialData,
737
+ adminConfig,
738
+ useAsTitle,
739
+ useAsPath,
740
+ headingLabel,
741
+ headerSlot,
742
+ collectionPath,
743
+ initialLocale,
744
+ onLocaleChange,
745
+ defaultLocale,
746
+ useNavigationGuard,
747
+ }: FormRendererProps) => {
748
+ // Persists per-tab-set active tab across locale-change remounts of FormContent.
749
+ // useRef so mutations never trigger a re-render of FormRenderer itself.
750
+ const savedTabsRef = useRef<Record<string, string>>({})
751
+
752
+ return (
753
+ <FormProvider
754
+ key={`${initialLocale ?? 'default'}-${initialData?.versionId ?? ''}`}
755
+ initialData={initialData}
756
+ >
757
+ <FormContent
758
+ mode={mode}
759
+ fields={fields}
760
+ onSubmit={onSubmit}
761
+ onCancel={onCancel}
762
+ onStatusChange={onStatusChange}
763
+ onUnpublish={onUnpublish}
764
+ onDelete={onDelete}
765
+ nextStatus={nextStatus}
766
+ workflowStatuses={workflowStatuses}
767
+ publishedVersion={publishedVersion}
768
+ initialData={initialData}
769
+ adminConfig={adminConfig}
770
+ useAsTitle={useAsTitle}
771
+ useAsPath={useAsPath}
772
+ headingLabel={headingLabel}
773
+ headerSlot={headerSlot}
774
+ collectionPath={collectionPath}
775
+ initialLocale={initialLocale}
776
+ onLocaleChange={onLocaleChange}
777
+ defaultLocale={defaultLocale}
778
+ useNavigationGuard={useNavigationGuard}
779
+ _activeTabBySet={savedTabsRef.current}
780
+ _onTabChange={(tabSetName, tabName) => {
781
+ savedTabsRef.current = { ...savedTabsRef.current, [tabSetName]: tabName }
782
+ }}
783
+ />
784
+ </FormProvider>
785
+ )
786
+ }