@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,98 @@
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
+ /**
10
+ * Framework-agnostic navigation guard adapter.
11
+ *
12
+ * Different router frameworks (TanStack Router, Next.js, React Router, etc.)
13
+ * each have their own mechanism for blocking navigation when a form has unsaved
14
+ * changes. This module defines a common interface so that `FormRenderer` can
15
+ * remain framework-independent — the consuming application injects the
16
+ * appropriate adapter via a prop or React context.
17
+ */
18
+
19
+ import { createContext, useContext, useEffect } from 'react'
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** The result returned by a `UseNavigationGuard` hook. */
26
+ export interface NavigationGuardResult {
27
+ /** Whether a navigation attempt is currently being blocked (show confirmation UI). */
28
+ isBlocked: boolean
29
+ /** Cancel the pending navigation — stay on the current page. */
30
+ stay: () => void
31
+ /** Confirm the pending navigation — leave the page. */
32
+ proceed: () => void
33
+ }
34
+
35
+ /**
36
+ * A hook that blocks in-app navigation and (optionally) browser unload when
37
+ * `shouldBlock` is `true`.
38
+ *
39
+ * Each framework adapter implements this signature.
40
+ */
41
+ export type UseNavigationGuard = (shouldBlock: boolean) => NavigationGuardResult
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Default (no-op) implementation — browser `beforeunload` only
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Fallback navigation guard that only handles the browser's native
49
+ * `beforeunload` event. In-app (client-side) route changes are **not**
50
+ * intercepted — `isBlocked` will never become `true`.
51
+ *
52
+ * This is used when no framework-specific adapter has been provided.
53
+ */
54
+ export const useBeforeUnloadGuard: UseNavigationGuard = (shouldBlock) => {
55
+ useEffect(() => {
56
+ if (!shouldBlock) return
57
+ const handler = (e: BeforeUnloadEvent) => {
58
+ e.preventDefault()
59
+ }
60
+ window.addEventListener('beforeunload', handler)
61
+ return () => window.removeEventListener('beforeunload', handler)
62
+ }, [shouldBlock])
63
+
64
+ return { isBlocked: false, stay: () => {}, proceed: () => {} }
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // React context — allows setting the adapter once at the app shell level
69
+ // ---------------------------------------------------------------------------
70
+
71
+ const NavigationGuardContext = createContext<UseNavigationGuard>(useBeforeUnloadGuard)
72
+
73
+ /**
74
+ * Provide a framework-specific `UseNavigationGuard` hook to all descendant
75
+ * `FormRenderer` instances.
76
+ *
77
+ * ```tsx
78
+ * import { NavigationGuardProvider } from './navigation-guard'
79
+ * import { useTanStackNavigationGuard } from './tanstack-navigation-guard'
80
+ *
81
+ * function App() {
82
+ * return (
83
+ * <NavigationGuardProvider value={useTanStackNavigationGuard}>
84
+ * <Outlet />
85
+ * </NavigationGuardProvider>
86
+ * )
87
+ * }
88
+ * ```
89
+ */
90
+ export const NavigationGuardProvider = NavigationGuardContext.Provider
91
+
92
+ /**
93
+ * Consume the current `UseNavigationGuard` hook from context.
94
+ * Falls back to `useBeforeUnloadGuard` when no provider is present.
95
+ */
96
+ export const useNavigationGuardAdapter = (): UseNavigationGuard => {
97
+ return useContext(NavigationGuardContext)
98
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * PathWidget — system-managed `documentVersions.path` form widget.
3
+ *
4
+ * Override handles:
5
+ * .byline-form-path — wrapper div
6
+ * .byline-form-path-header — label + regenerate-button row
7
+ * .byline-form-path-regenerate — regenerate-link button
8
+ * .byline-form-path-sr-only — visually-hidden screen-reader hint
9
+ */
10
+
11
+ .header,
12
+ :global(.byline-form-path-header) {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ gap: var(--spacing-8);
17
+ }
18
+
19
+ .regenerate,
20
+ :global(.byline-form-path-regenerate) {
21
+ background: none;
22
+ border: none;
23
+ padding: 0;
24
+ color: inherit;
25
+ font-size: 0.8rem;
26
+ text-decoration: underline;
27
+ cursor: pointer;
28
+ }
29
+
30
+ .sr-only,
31
+ :global(.byline-form-path-sr-only) {
32
+ position: absolute;
33
+ width: 1px;
34
+ height: 1px;
35
+ padding: 0;
36
+ margin: -1px;
37
+ overflow: hidden;
38
+ clip: rect(0, 0, 0, 0);
39
+ white-space: nowrap;
40
+ border: 0;
41
+ }
@@ -0,0 +1,217 @@
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 { act } from 'react'
10
+
11
+ import { createRoot, type Root } from 'react-dom/client'
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
13
+
14
+ // Lightweight uikit stubs — we don't care about the visual rendering, only
15
+ // that the Input forwards props and the Label renders its htmlFor.
16
+ vi.mock('@infonomic/uikit/react', () => ({
17
+ Label: ({ id, htmlFor, label }: { id?: string; htmlFor?: string; label?: string }) => (
18
+ <label id={id} htmlFor={htmlFor}>
19
+ {label}
20
+ </label>
21
+ ),
22
+ Input: ({
23
+ id,
24
+ name,
25
+ value,
26
+ placeholder,
27
+ onChange,
28
+ helpText,
29
+ ...rest
30
+ }: {
31
+ id?: string
32
+ name?: string
33
+ value?: string
34
+ placeholder?: string
35
+ onChange?: (e: { target: { value: string } }) => void
36
+ helpText?: string
37
+ [key: string]: any
38
+ }) => (
39
+ <>
40
+ <input
41
+ id={id}
42
+ name={name}
43
+ value={value ?? ''}
44
+ placeholder={placeholder}
45
+ onChange={onChange}
46
+ {...rest}
47
+ />
48
+ {helpText ? <span data-testid="help-text">{helpText}</span> : null}
49
+ </>
50
+ ),
51
+ }))
52
+
53
+ // Mutable mocks controlled per-test via the setFixture helper below.
54
+ const fixture: {
55
+ systemPath: string | null
56
+ sourceValue: unknown
57
+ setSystemPath: ReturnType<typeof vi.fn>
58
+ } = {
59
+ systemPath: null,
60
+ sourceValue: '',
61
+ setSystemPath: vi.fn(),
62
+ }
63
+
64
+ vi.mock('./form-context', () => ({
65
+ useFormContext: () => ({ setSystemPath: fixture.setSystemPath }),
66
+ useSystemPath: () => fixture.systemPath,
67
+ useFieldValue: () => fixture.sourceValue,
68
+ }))
69
+
70
+ // Import AFTER the mocks so PathWidget picks them up.
71
+ // biome-ignore lint/correctness/useImportExtensions: webapp TS resolves via tsconfig paths
72
+ import { PathWidget } from './path-widget'
73
+
74
+ ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
75
+
76
+ interface Fixture {
77
+ systemPath?: string | null
78
+ sourceValue?: unknown
79
+ }
80
+
81
+ function setFixture(next: Fixture = {}) {
82
+ fixture.systemPath = next.systemPath ?? null
83
+ fixture.sourceValue = next.sourceValue ?? ''
84
+ fixture.setSystemPath = vi.fn()
85
+ }
86
+
87
+ describe('PathWidget', () => {
88
+ let container: HTMLDivElement
89
+ let root: Root
90
+
91
+ beforeEach(() => {
92
+ container = document.createElement('div')
93
+ document.body.appendChild(container)
94
+ root = createRoot(container)
95
+ })
96
+
97
+ afterEach(() => {
98
+ act(() => {
99
+ root.unmount()
100
+ })
101
+ container.remove()
102
+ })
103
+
104
+ const render = (
105
+ props: Partial<{
106
+ useAsPath: string | undefined
107
+ mode: 'create' | 'edit'
108
+ }> = {}
109
+ ) => {
110
+ act(() => {
111
+ root.render(
112
+ <PathWidget
113
+ useAsPath={props.useAsPath ?? 'title'}
114
+ collectionPath="pages"
115
+ defaultLocale="en"
116
+ mode={props.mode ?? 'create'}
117
+ />
118
+ )
119
+ })
120
+ }
121
+
122
+ const getInput = () => container.querySelector('#system-path') as HTMLInputElement
123
+
124
+ it('shows the live-derived preview as placeholder when creating with an empty override', () => {
125
+ setFixture({ systemPath: null, sourceValue: 'Hello World' })
126
+ render({ mode: 'create' })
127
+
128
+ const input = getInput()
129
+ expect(input).toBeTruthy()
130
+ expect(input.value).toBe('')
131
+ expect(input.getAttribute('placeholder')).toBe('Will be saved as "hello-world"')
132
+ })
133
+
134
+ it('shows the persisted path in edit mode (no placeholder preview)', () => {
135
+ setFixture({ systemPath: 'existing-path', sourceValue: 'Hello World' })
136
+ render({ mode: 'edit' })
137
+
138
+ const input = getInput()
139
+ expect(input.value).toBe('existing-path')
140
+ // livePreview === 'hello-world' differs from persisted 'existing-path',
141
+ // so the regenerate button is rendered.
142
+ const regenerate = container.querySelector('button')
143
+ expect(regenerate).toBeTruthy()
144
+ expect(regenerate?.textContent).toContain('Regenerate from title')
145
+ })
146
+
147
+ it('"Regenerate" writes the live preview into the systemPath slot', () => {
148
+ setFixture({ systemPath: 'stale-path', sourceValue: 'Brand New Title' })
149
+ render({ mode: 'edit' })
150
+
151
+ const regenerate = container.querySelector('button') as HTMLButtonElement
152
+ expect(regenerate).toBeTruthy()
153
+
154
+ act(() => {
155
+ regenerate.click()
156
+ })
157
+
158
+ expect(fixture.setSystemPath).toHaveBeenCalledWith('brand-new-title')
159
+ })
160
+
161
+ it('clearing the input reverts the slot to null (sticky-from-previous)', () => {
162
+ setFixture({ systemPath: 'my-path', sourceValue: 'My Path' })
163
+ render({ mode: 'edit' })
164
+
165
+ const input = getInput()
166
+ act(() => {
167
+ const setter = Object.getOwnPropertyDescriptor(
168
+ window.HTMLInputElement.prototype,
169
+ 'value'
170
+ )?.set
171
+ setter?.call(input, '')
172
+ input.dispatchEvent(new Event('input', { bubbles: true }))
173
+ })
174
+
175
+ expect(fixture.setSystemPath).toHaveBeenCalledWith(null)
176
+ })
177
+
178
+ it('typing a non-empty value writes a string override', () => {
179
+ setFixture({ systemPath: null, sourceValue: 'Anything' })
180
+ render({ mode: 'create' })
181
+
182
+ const input = getInput()
183
+ act(() => {
184
+ const setter = Object.getOwnPropertyDescriptor(
185
+ window.HTMLInputElement.prototype,
186
+ 'value'
187
+ )?.set
188
+ setter?.call(input, 'custom-slug')
189
+ input.dispatchEvent(new Event('input', { bubbles: true }))
190
+ })
191
+
192
+ expect(fixture.setSystemPath).toHaveBeenCalledWith('custom-slug')
193
+ })
194
+
195
+ it('links the input to an sr-only description via aria-describedby', () => {
196
+ setFixture({ systemPath: null, sourceValue: 'Hi' })
197
+ render({ mode: 'create' })
198
+
199
+ const input = getInput()
200
+ expect(input.getAttribute('aria-describedby')).toBe('system-path-description')
201
+ const description = container.querySelector('#system-path-description')
202
+ expect(description).toBeTruthy()
203
+ expect(description?.textContent).toContain('System-managed URL path')
204
+ })
205
+
206
+ it('does not render the Regenerate button when livePreview equals systemPath', () => {
207
+ setFixture({ systemPath: 'hello-world', sourceValue: 'Hello World' })
208
+ render({ mode: 'edit' })
209
+ expect(container.querySelector('button')).toBeNull()
210
+ })
211
+
212
+ it('does not render the Regenerate button when there is no useAsPath', () => {
213
+ setFixture({ systemPath: 'whatever', sourceValue: '' })
214
+ render({ mode: 'edit', useAsPath: undefined })
215
+ expect(container.querySelector('button')).toBeNull()
216
+ })
217
+ })
@@ -0,0 +1,141 @@
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 { useCallback, useMemo } from 'react'
10
+
11
+ import { slugify } from '@byline/core'
12
+ import { Input, Label } from '@infonomic/uikit/react'
13
+ import cx from 'classnames'
14
+
15
+ import { useFieldValue, useFormContext, useSystemPath } from './form-context'
16
+ import styles from './path-widget.module.css'
17
+
18
+ /**
19
+ * Coerce an arbitrary source-field value (string, Date, or other) into
20
+ * a string suitable for slugification. Mirrors the lifecycle's coercion
21
+ * so the live preview matches what the server will store.
22
+ */
23
+ function coerceToString(value: unknown): string {
24
+ if (value == null) return ''
25
+ if (value instanceof Date) return value.toISOString()
26
+ return String(value)
27
+ }
28
+
29
+ export interface PathWidgetProps {
30
+ /** The collection's `useAsPath` source field name, when configured. */
31
+ useAsPath: string | undefined
32
+ /** Collection path, forwarded to the slugifier as context. */
33
+ collectionPath: string
34
+ /** Default content locale, forwarded to the slugifier as context. */
35
+ defaultLocale: string
36
+ /** `'create'` shows the live derived preview as placeholder text. */
37
+ mode: 'create' | 'edit'
38
+ }
39
+
40
+ /**
41
+ * System-managed `documentVersions.path` widget.
42
+ *
43
+ * Displays the current persisted/overridden path as an editable input.
44
+ * In create mode, when the user hasn't supplied an override, the input
45
+ * shows the live-derived preview (slugified `useAsPath` source field) as
46
+ * a placeholder so the user sees what will be saved. The "Regenerate"
47
+ * action explicitly writes the current live preview into the override
48
+ * slot so the user can re-anchor a path against the source field after
49
+ * editing the title.
50
+ *
51
+ * Stable override handles: `.byline-form-path`, `.byline-form-path-header`,
52
+ * `.byline-form-path-regenerate`.
53
+ */
54
+ export const PathWidget = ({ useAsPath, collectionPath, defaultLocale, mode }: PathWidgetProps) => {
55
+ const { setSystemPath } = useFormContext()
56
+ const systemPath = useSystemPath()
57
+ const sourceValue = useFieldValue<unknown>(useAsPath ?? '')
58
+
59
+ // Live preview — what the server would derive from the current source
60
+ // field value if no override were set. Used as placeholder in create
61
+ // mode and as the target of the "Regenerate" action.
62
+ const livePreview = useMemo(() => {
63
+ if (!useAsPath) return ''
64
+ const asString = coerceToString(sourceValue)
65
+ if (asString.length === 0) return ''
66
+ return slugify(asString, { locale: defaultLocale, collectionPath })
67
+ }, [useAsPath, sourceValue, defaultLocale, collectionPath])
68
+
69
+ const inputValue = systemPath ?? ''
70
+
71
+ const handleChange = useCallback(
72
+ (next: string) => {
73
+ // Empty string clears the override — server falls back to derive
74
+ // (create) or sticky (update).
75
+ setSystemPath(next.length === 0 ? null : next)
76
+ },
77
+ [setSystemPath]
78
+ )
79
+
80
+ const handleRegenerate = useCallback(() => {
81
+ if (livePreview.length > 0) {
82
+ setSystemPath(livePreview)
83
+ }
84
+ }, [livePreview, setSystemPath])
85
+
86
+ // Validate live: if the typed value differs from its slugified form,
87
+ // surface an inline hint without blocking input (mirrors the previous
88
+ // field-hook advisory behaviour).
89
+ const formatted = useMemo(() => {
90
+ if (inputValue.length === 0) return ''
91
+ return slugify(inputValue, { locale: defaultLocale, collectionPath })
92
+ }, [inputValue, defaultLocale, collectionPath])
93
+
94
+ const hint =
95
+ inputValue.length > 0 && formatted !== inputValue ? `Suggested: "${formatted}"` : undefined
96
+
97
+ const placeholder =
98
+ mode === 'create' && livePreview.length > 0 ? `Will be saved as "${livePreview}"` : undefined
99
+
100
+ // Screen-reader description. The input's base purpose ("System-managed
101
+ // URL path") plus whichever of the visible hints (placeholder preview
102
+ // in create mode, "Suggested" validation hint) currently applies. The
103
+ // visible helpText/placeholder cover sighted users; this element makes
104
+ // the same information addressable via aria-describedby for AT.
105
+ const srDescription = ['System-managed URL path for this document.', placeholder, hint]
106
+ .filter(Boolean)
107
+ .join(' ')
108
+
109
+ return (
110
+ <div className="byline-form-path">
111
+ <div className={cx('byline-form-path-header', styles.header)}>
112
+ <Label id="system-path-label" htmlFor="system-path" label="Path" />
113
+ {useAsPath && livePreview.length > 0 && livePreview !== systemPath && (
114
+ <button
115
+ type="button"
116
+ onClick={handleRegenerate}
117
+ className={cx('byline-form-path-regenerate', styles.regenerate)}
118
+ aria-label={`Regenerate path from ${useAsPath} field`}
119
+ >
120
+ Regenerate from {useAsPath}
121
+ </button>
122
+ )}
123
+ </div>
124
+ <Input
125
+ id="system-path"
126
+ name="__systemPath__"
127
+ value={inputValue}
128
+ placeholder={placeholder}
129
+ onChange={(e) => handleChange(e.target.value)}
130
+ helpText={hint}
131
+ aria-describedby="system-path-description"
132
+ />
133
+ <span
134
+ id="system-path-description"
135
+ className={cx('byline-form-path-sr-only', styles['sr-only'])}
136
+ >
137
+ {srDescription}
138
+ </span>
139
+ </div>
140
+ )
141
+ }
@@ -0,0 +1,190 @@
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
+ /**
10
+ * Upload Executor
11
+ *
12
+ * Handles batch execution of pending file uploads at form submission time.
13
+ * This enables "deferred uploads" — files are selected/previewed immediately
14
+ * but only uploaded when the user clicks Save.
15
+ */
16
+
17
+ import type { StoredFileValue } from '@byline/core'
18
+
19
+ import type { UploadFieldFn } from '../services/field-services-types'
20
+ import type { PendingUpload } from './form-context'
21
+
22
+ export interface UploadResult {
23
+ fieldPath: string
24
+ success: boolean
25
+ storedFile?: StoredFileValue
26
+ error?: string
27
+ }
28
+
29
+ export interface ExecuteUploadsResult {
30
+ /** All upload results (both successful and failed) */
31
+ results: UploadResult[]
32
+ /** Map of field path to StoredFileValue for successful uploads */
33
+ successful: Map<string, StoredFileValue>
34
+ /** Map of field path to error message for failed uploads */
35
+ errors: Map<string, string>
36
+ /** Whether all uploads succeeded */
37
+ allSucceeded: boolean
38
+ }
39
+
40
+ /**
41
+ * Execute all pending uploads sequentially.
42
+ * Returns a result object with successful uploads and any errors.
43
+ *
44
+ * @param pendingUploads - Map of field path to PendingUpload
45
+ * @param uploadField - Host-provided upload transport (resolved via
46
+ * `useBylineFieldServices()` in the calling React tree)
47
+ * @returns Promise resolving to ExecuteUploadsResult
48
+ */
49
+ export async function executeUploads(
50
+ pendingUploads: Map<string, PendingUpload>,
51
+ uploadField: UploadFieldFn
52
+ ): Promise<ExecuteUploadsResult> {
53
+ const results: UploadResult[] = []
54
+ const successful = new Map<string, StoredFileValue>()
55
+ const errors = new Map<string, string>()
56
+
57
+ for (const [fieldPath, upload] of pendingUploads.entries()) {
58
+ const formData = new FormData()
59
+ formData.append('file', upload.file)
60
+ // Tell the server which upload-capable field this file belongs to.
61
+ // With per-field upload config a collection can have multiple
62
+ // image/file fields, each with its own constraints; the server's
63
+ // unique-default fallback covers the single-field case but rejects
64
+ // multi-field collections without an explicit selector.
65
+ formData.append('field', uploadFieldName(fieldPath))
66
+
67
+ try {
68
+ // Pass createDocument=false — we're uploading for an embedded field,
69
+ // the form's save action handles document creation/update.
70
+ const result = await uploadField(upload.collectionPath, formData, false)
71
+
72
+ results.push({
73
+ fieldPath,
74
+ success: true,
75
+ storedFile: result.storedFile,
76
+ })
77
+ successful.set(fieldPath, result.storedFile)
78
+ } catch (err: unknown) {
79
+ const message = err instanceof Error ? err.message : 'Upload failed'
80
+ results.push({
81
+ fieldPath,
82
+ success: false,
83
+ error: message,
84
+ })
85
+ errors.set(fieldPath, message)
86
+ }
87
+ }
88
+
89
+ return {
90
+ results,
91
+ successful,
92
+ errors,
93
+ allSucceeded: errors.size === 0,
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Extract the leaf field name from a `fieldPath`. Top-level upload
99
+ * fields (`'image'`, `'avatar'`) pass through unchanged; nested paths
100
+ * (`'profile.avatar'`) reduce to their last segment, since the
101
+ * server-side resolver matches against top-level field names today.
102
+ * Nested upload fields would need a richer transport selector when
103
+ * they land — the host resolver is the natural place to extend.
104
+ */
105
+ function uploadFieldName(fieldPath: string): string {
106
+ const dot = fieldPath.lastIndexOf('.')
107
+ return dot === -1 ? fieldPath : fieldPath.slice(dot + 1)
108
+ }
109
+
110
+ /**
111
+ * Progress callback type for upload execution with progress tracking.
112
+ */
113
+ export type UploadProgressCallback = (info: {
114
+ current: number
115
+ total: number
116
+ fieldPath: string
117
+ status: 'uploading' | 'done' | 'error'
118
+ }) => void
119
+
120
+ /**
121
+ * Execute uploads with progress callbacks.
122
+ * Useful for showing upload progress in the UI.
123
+ */
124
+ export async function executeUploadsWithProgress(
125
+ pendingUploads: Map<string, PendingUpload>,
126
+ uploadField: UploadFieldFn,
127
+ onProgress?: UploadProgressCallback
128
+ ): Promise<ExecuteUploadsResult> {
129
+ const results: UploadResult[] = []
130
+ const successful = new Map<string, StoredFileValue>()
131
+ const errors = new Map<string, string>()
132
+
133
+ const entries = Array.from(pendingUploads.entries())
134
+ const total = entries.length
135
+
136
+ for (let i = 0; i < entries.length; i++) {
137
+ const [fieldPath, upload] = entries[i]
138
+
139
+ onProgress?.({
140
+ current: i + 1,
141
+ total,
142
+ fieldPath,
143
+ status: 'uploading',
144
+ })
145
+
146
+ const formData = new FormData()
147
+ formData.append('file', upload.file)
148
+ formData.append('field', uploadFieldName(fieldPath))
149
+
150
+ try {
151
+ const result = await uploadField(upload.collectionPath, formData, false)
152
+
153
+ results.push({
154
+ fieldPath,
155
+ success: true,
156
+ storedFile: result.storedFile,
157
+ })
158
+ successful.set(fieldPath, result.storedFile)
159
+
160
+ onProgress?.({
161
+ current: i + 1,
162
+ total,
163
+ fieldPath,
164
+ status: 'done',
165
+ })
166
+ } catch (err: unknown) {
167
+ const message = err instanceof Error ? err.message : 'Upload failed'
168
+ results.push({
169
+ fieldPath,
170
+ success: false,
171
+ error: message,
172
+ })
173
+ errors.set(fieldPath, message)
174
+
175
+ onProgress?.({
176
+ current: i + 1,
177
+ total,
178
+ fieldPath,
179
+ status: 'error',
180
+ })
181
+ }
182
+ }
183
+
184
+ return {
185
+ results,
186
+ successful,
187
+ errors,
188
+ allSucceeded: errors.size === 0,
189
+ }
190
+ }