@pilotiq/pilotiq 0.7.2 → 0.8.1

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 (371) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +208 -0
  3. package/CLAUDE.md +59 -3
  4. package/dist/Pilotiq.d.ts +83 -0
  5. package/dist/Pilotiq.d.ts.map +1 -1
  6. package/dist/Pilotiq.js +39 -0
  7. package/dist/Pilotiq.js.map +1 -1
  8. package/dist/actions/Action.d.ts +27 -99
  9. package/dist/actions/Action.d.ts.map +1 -1
  10. package/dist/actions/Action.js +52 -754
  11. package/dist/actions/Action.js.map +1 -1
  12. package/dist/actions/bulkFactories.d.ts +46 -0
  13. package/dist/actions/bulkFactories.d.ts.map +1 -0
  14. package/dist/actions/bulkFactories.js +144 -0
  15. package/dist/actions/bulkFactories.js.map +1 -0
  16. package/dist/actions/crudFactories.d.ts +94 -0
  17. package/dist/actions/crudFactories.d.ts.map +1 -0
  18. package/dist/actions/crudFactories.js +209 -0
  19. package/dist/actions/crudFactories.js.map +1 -0
  20. package/dist/actions/factoryHelpers.d.ts +108 -0
  21. package/dist/actions/factoryHelpers.d.ts.map +1 -0
  22. package/dist/actions/factoryHelpers.js +138 -0
  23. package/dist/actions/factoryHelpers.js.map +1 -0
  24. package/dist/actions/m2mFactories.d.ts +47 -0
  25. package/dist/actions/m2mFactories.d.ts.map +1 -0
  26. package/dist/actions/m2mFactories.js +173 -0
  27. package/dist/actions/m2mFactories.js.map +1 -0
  28. package/dist/actions/relationFactories.d.ts +93 -0
  29. package/dist/actions/relationFactories.d.ts.map +1 -0
  30. package/dist/actions/relationFactories.js +321 -0
  31. package/dist/actions/relationFactories.js.map +1 -0
  32. package/dist/elements/dispatchForm.js +1 -1
  33. package/dist/elements/dispatchForm.js.map +1 -1
  34. package/dist/elements/dispatchTable.js +1 -1
  35. package/dist/elements/dispatchTable.js.map +1 -1
  36. package/dist/fields/Field.d.ts +31 -0
  37. package/dist/fields/Field.d.ts.map +1 -1
  38. package/dist/fields/Field.js +25 -0
  39. package/dist/fields/Field.js.map +1 -1
  40. package/dist/pageData/breadcrumbs.d.ts +42 -0
  41. package/dist/pageData/breadcrumbs.d.ts.map +1 -0
  42. package/dist/pageData/breadcrumbs.js +172 -0
  43. package/dist/pageData/breadcrumbs.js.map +1 -0
  44. package/dist/pageData/forms.d.ts +137 -0
  45. package/dist/pageData/forms.d.ts.map +1 -0
  46. package/dist/pageData/forms.js +427 -0
  47. package/dist/pageData/forms.js.map +1 -0
  48. package/dist/pageData/helpers.d.ts +239 -0
  49. package/dist/pageData/helpers.d.ts.map +1 -0
  50. package/dist/pageData/helpers.js +703 -0
  51. package/dist/pageData/helpers.js.map +1 -0
  52. package/dist/pageData/misc.d.ts +76 -0
  53. package/dist/pageData/misc.d.ts.map +1 -0
  54. package/dist/pageData/misc.js +263 -0
  55. package/dist/pageData/misc.js.map +1 -0
  56. package/dist/pageData/navigation.d.ts +292 -0
  57. package/dist/pageData/navigation.d.ts.map +1 -0
  58. package/dist/pageData/navigation.js +591 -0
  59. package/dist/pageData/navigation.js.map +1 -0
  60. package/dist/pageData/relationPages.d.ts +172 -0
  61. package/dist/pageData/relationPages.d.ts.map +1 -0
  62. package/dist/pageData/relationPages.js +867 -0
  63. package/dist/pageData/relationPages.js.map +1 -0
  64. package/dist/pageData/relationTabs.d.ts +65 -0
  65. package/dist/pageData/relationTabs.d.ts.map +1 -0
  66. package/dist/pageData/relationTabs.js +258 -0
  67. package/dist/pageData/relationTabs.js.map +1 -0
  68. package/dist/pageData/resourcePages.d.ts +48 -0
  69. package/dist/pageData/resourcePages.d.ts.map +1 -0
  70. package/dist/pageData/resourcePages.js +504 -0
  71. package/dist/pageData/resourcePages.js.map +1 -0
  72. package/dist/pageData.d.ts +12 -792
  73. package/dist/pageData.d.ts.map +1 -1
  74. package/dist/pageData.js +24 -3797
  75. package/dist/pageData.js.map +1 -1
  76. package/dist/react/AppShell.d.ts +8 -0
  77. package/dist/react/AppShell.d.ts.map +1 -1
  78. package/dist/react/AppShell.js +11 -1
  79. package/dist/react/AppShell.js.map +1 -1
  80. package/dist/react/CollabExtensionFactoryRegistry.d.ts +47 -0
  81. package/dist/react/CollabExtensionFactoryRegistry.d.ts.map +1 -0
  82. package/dist/react/CollabExtensionFactoryRegistry.js +14 -0
  83. package/dist/react/CollabExtensionFactoryRegistry.js.map +1 -0
  84. package/dist/react/CollabRoomContext.d.ts +37 -0
  85. package/dist/react/CollabRoomContext.d.ts.map +1 -0
  86. package/dist/react/CollabRoomContext.js +12 -0
  87. package/dist/react/CollabRoomContext.js.map +1 -0
  88. package/dist/react/FormCollabBindingRegistry.d.ts +62 -0
  89. package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -0
  90. package/dist/react/FormCollabBindingRegistry.js +14 -0
  91. package/dist/react/FormCollabBindingRegistry.js.map +1 -0
  92. package/dist/react/FormStateContext.d.ts.map +1 -1
  93. package/dist/react/FormStateContext.js +87 -0
  94. package/dist/react/FormStateContext.js.map +1 -1
  95. package/dist/react/RecordWrapperGate.d.ts +25 -0
  96. package/dist/react/RecordWrapperGate.d.ts.map +1 -0
  97. package/dist/react/RecordWrapperGate.js +30 -0
  98. package/dist/react/RecordWrapperGate.js.map +1 -0
  99. package/dist/react/RecordWrapperRegistry.d.ts +31 -0
  100. package/dist/react/RecordWrapperRegistry.d.ts.map +1 -0
  101. package/dist/react/RecordWrapperRegistry.js +15 -0
  102. package/dist/react/RecordWrapperRegistry.js.map +1 -0
  103. package/dist/react/SchemaRenderer.d.ts +17 -23
  104. package/dist/react/SchemaRenderer.d.ts.map +1 -1
  105. package/dist/react/SchemaRenderer.js +71 -3647
  106. package/dist/react/SchemaRenderer.js.map +1 -1
  107. package/dist/react/component-slots.d.ts +103 -0
  108. package/dist/react/component-slots.d.ts.map +1 -0
  109. package/dist/react/component-slots.js +18 -0
  110. package/dist/react/component-slots.js.map +1 -0
  111. package/dist/react/fields/BuilderInput.d.ts.map +1 -1
  112. package/dist/react/fields/BuilderInput.js +21 -117
  113. package/dist/react/fields/BuilderInput.js.map +1 -1
  114. package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
  115. package/dist/react/fields/MarkdownInput.js +1 -3
  116. package/dist/react/fields/MarkdownInput.js.map +1 -1
  117. package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
  118. package/dist/react/fields/RepeaterInput.js +22 -127
  119. package/dist/react/fields/RepeaterInput.js.map +1 -1
  120. package/dist/react/fields/rowState.d.ts +40 -0
  121. package/dist/react/fields/rowState.d.ts.map +1 -0
  122. package/dist/react/fields/rowState.js +60 -0
  123. package/dist/react/fields/rowState.js.map +1 -0
  124. package/dist/react/fields/useRowReorderDnd.d.ts +28 -0
  125. package/dist/react/fields/useRowReorderDnd.d.ts.map +1 -0
  126. package/dist/react/fields/useRowReorderDnd.js +51 -0
  127. package/dist/react/fields/useRowReorderDnd.js.map +1 -0
  128. package/dist/react/index.d.ts +9 -0
  129. package/dist/react/index.d.ts.map +1 -1
  130. package/dist/react/index.js +8 -0
  131. package/dist/react/index.js.map +1 -1
  132. package/dist/react/layouts/SidebarLayout.d.ts +1 -1
  133. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  134. package/dist/react/layouts/SidebarLayout.js +10 -2
  135. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  136. package/dist/react/layouts/TopbarLayout.d.ts +1 -1
  137. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -1
  138. package/dist/react/layouts/TopbarLayout.js +19 -11
  139. package/dist/react/layouts/TopbarLayout.js.map +1 -1
  140. package/dist/react/parseRecordEditUrl.d.ts +29 -0
  141. package/dist/react/parseRecordEditUrl.d.ts.map +1 -0
  142. package/dist/react/parseRecordEditUrl.js +25 -0
  143. package/dist/react/parseRecordEditUrl.js.map +1 -0
  144. package/dist/react/persistedState.d.ts +19 -0
  145. package/dist/react/persistedState.d.ts.map +1 -0
  146. package/dist/react/persistedState.js +51 -0
  147. package/dist/react/persistedState.js.map +1 -0
  148. package/dist/react/schemaRenderer/AlertRenderer.d.ts +12 -0
  149. package/dist/react/schemaRenderer/AlertRenderer.d.ts.map +1 -0
  150. package/dist/react/schemaRenderer/AlertRenderer.js +61 -0
  151. package/dist/react/schemaRenderer/AlertRenderer.js.map +1 -0
  152. package/dist/react/schemaRenderer/EntryRenderer.d.ts +13 -0
  153. package/dist/react/schemaRenderer/EntryRenderer.d.ts.map +1 -0
  154. package/dist/react/schemaRenderer/EntryRenderer.js +277 -0
  155. package/dist/react/schemaRenderer/EntryRenderer.js.map +1 -0
  156. package/dist/react/schemaRenderer/SectionRenderer.d.ts +16 -0
  157. package/dist/react/schemaRenderer/SectionRenderer.d.ts.map +1 -0
  158. package/dist/react/schemaRenderer/SectionRenderer.js +62 -0
  159. package/dist/react/schemaRenderer/SectionRenderer.js.map +1 -0
  160. package/dist/react/schemaRenderer/SimpleElements.d.ts +25 -0
  161. package/dist/react/schemaRenderer/SimpleElements.d.ts.map +1 -0
  162. package/dist/react/schemaRenderer/SimpleElements.js +147 -0
  163. package/dist/react/schemaRenderer/SimpleElements.js.map +1 -0
  164. package/dist/react/schemaRenderer/TabsRenderer.d.ts +17 -0
  165. package/dist/react/schemaRenderer/TabsRenderer.d.ts.map +1 -0
  166. package/dist/react/schemaRenderer/TabsRenderer.js +31 -0
  167. package/dist/react/schemaRenderer/TabsRenderer.js.map +1 -0
  168. package/dist/react/schemaRenderer/WizardRenderer.d.ts +34 -0
  169. package/dist/react/schemaRenderer/WizardRenderer.d.ts.map +1 -0
  170. package/dist/react/schemaRenderer/WizardRenderer.js +208 -0
  171. package/dist/react/schemaRenderer/WizardRenderer.js.map +1 -0
  172. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts +21 -0
  173. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts.map +1 -0
  174. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js +82 -0
  175. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js.map +1 -0
  176. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts +30 -0
  177. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts.map +1 -0
  178. package/dist/react/schemaRenderer/action/ActionModalDialog.js +182 -0
  179. package/dist/react/schemaRenderer/action/ActionModalDialog.js.map +1 -0
  180. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts +17 -0
  181. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts.map +1 -0
  182. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js +19 -0
  183. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js.map +1 -0
  184. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts +16 -0
  185. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts.map +1 -0
  186. package/dist/react/schemaRenderer/action/HandlerActionButton.js +16 -0
  187. package/dist/react/schemaRenderer/action/HandlerActionButton.js.map +1 -0
  188. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts +22 -0
  189. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts.map +1 -0
  190. package/dist/react/schemaRenderer/action/MethodActionButton.js +26 -0
  191. package/dist/react/schemaRenderer/action/MethodActionButton.js.map +1 -0
  192. package/dist/react/schemaRenderer/action/buttons.d.ts +18 -0
  193. package/dist/react/schemaRenderer/action/buttons.d.ts.map +1 -0
  194. package/dist/react/schemaRenderer/action/buttons.js +74 -0
  195. package/dist/react/schemaRenderer/action/buttons.js.map +1 -0
  196. package/dist/react/schemaRenderer/action/helpers.d.ts +26 -0
  197. package/dist/react/schemaRenderer/action/helpers.d.ts.map +1 -0
  198. package/dist/react/schemaRenderer/action/helpers.js +126 -0
  199. package/dist/react/schemaRenderer/action/helpers.js.map +1 -0
  200. package/dist/react/schemaRenderer/action/renderAction.d.ts +21 -0
  201. package/dist/react/schemaRenderer/action/renderAction.d.ts.map +1 -0
  202. package/dist/react/schemaRenderer/action/renderAction.js +102 -0
  203. package/dist/react/schemaRenderer/action/renderAction.js.map +1 -0
  204. package/dist/react/schemaRenderer/columnFormat.d.ts +10 -0
  205. package/dist/react/schemaRenderer/columnFormat.d.ts.map +1 -0
  206. package/dist/react/schemaRenderer/columnFormat.js +76 -0
  207. package/dist/react/schemaRenderer/columnFormat.js.map +1 -0
  208. package/dist/react/schemaRenderer/constants.d.ts +8 -0
  209. package/dist/react/schemaRenderer/constants.d.ts.map +1 -0
  210. package/dist/react/schemaRenderer/constants.js +45 -0
  211. package/dist/react/schemaRenderer/constants.js.map +1 -0
  212. package/dist/react/schemaRenderer/form/FormRenderer.d.ts +29 -0
  213. package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -0
  214. package/dist/react/schemaRenderer/form/FormRenderer.js +163 -0
  215. package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -0
  216. package/dist/react/schemaRenderer/form/renderField.d.ts +6 -0
  217. package/dist/react/schemaRenderer/form/renderField.d.ts.map +1 -0
  218. package/dist/react/schemaRenderer/form/renderField.js +239 -0
  219. package/dist/react/schemaRenderer/form/renderField.js.map +1 -0
  220. package/dist/react/schemaRenderer/helpers.d.ts +32 -0
  221. package/dist/react/schemaRenderer/helpers.d.ts.map +1 -0
  222. package/dist/react/schemaRenderer/helpers.js +52 -0
  223. package/dist/react/schemaRenderer/helpers.js.map +1 -0
  224. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts +60 -0
  225. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts.map +1 -0
  226. package/dist/react/schemaRenderer/table/CardsLayoutBody.js +189 -0
  227. package/dist/react/schemaRenderer/table/CardsLayoutBody.js.map +1 -0
  228. package/dist/react/schemaRenderer/table/TableRenderer.d.ts +29 -0
  229. package/dist/react/schemaRenderer/table/TableRenderer.d.ts.map +1 -0
  230. package/dist/react/schemaRenderer/table/TableRenderer.js +85 -0
  231. package/dist/react/schemaRenderer/table/TableRenderer.js.map +1 -0
  232. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts +18 -0
  233. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts.map +1 -0
  234. package/dist/react/schemaRenderer/table/TableRendererBody.js +555 -0
  235. package/dist/react/schemaRenderer/table/TableRendererBody.js.map +1 -0
  236. package/dist/react/schemaRenderer/table/filters.d.ts +263 -0
  237. package/dist/react/schemaRenderer/table/filters.d.ts.map +1 -0
  238. package/dist/react/schemaRenderer/table/filters.js +497 -0
  239. package/dist/react/schemaRenderer/table/filters.js.map +1 -0
  240. package/dist/react/schemaRenderer/table/formatCell.d.ts +11 -0
  241. package/dist/react/schemaRenderer/table/formatCell.d.ts.map +1 -0
  242. package/dist/react/schemaRenderer/table/formatCell.js +172 -0
  243. package/dist/react/schemaRenderer/table/formatCell.js.map +1 -0
  244. package/dist/react/schemaRenderer/table/links.d.ts +42 -0
  245. package/dist/react/schemaRenderer/table/links.d.ts.map +1 -0
  246. package/dist/react/schemaRenderer/table/links.js +55 -0
  247. package/dist/react/schemaRenderer/table/links.js.map +1 -0
  248. package/dist/react/schemaRenderer/table/renderRowActions.d.ts +13 -0
  249. package/dist/react/schemaRenderer/table/renderRowActions.d.ts.map +1 -0
  250. package/dist/react/schemaRenderer/table/renderRowActions.js +25 -0
  251. package/dist/react/schemaRenderer/table/renderRowActions.js.map +1 -0
  252. package/dist/react/schemaRenderer/table/url.d.ts +41 -0
  253. package/dist/react/schemaRenderer/table/url.d.ts.map +1 -0
  254. package/dist/react/schemaRenderer/table/url.js +114 -0
  255. package/dist/react/schemaRenderer/table/url.js.map +1 -0
  256. package/dist/routes/globals.d.ts +13 -0
  257. package/dist/routes/globals.d.ts.map +1 -0
  258. package/dist/routes/globals.js +131 -0
  259. package/dist/routes/globals.js.map +1 -0
  260. package/dist/routes/helpers.d.ts +217 -0
  261. package/dist/routes/helpers.d.ts.map +1 -0
  262. package/dist/routes/helpers.js +498 -0
  263. package/dist/routes/helpers.js.map +1 -0
  264. package/dist/routes/pages.d.ts +15 -0
  265. package/dist/routes/pages.d.ts.map +1 -0
  266. package/dist/routes/pages.js +145 -0
  267. package/dist/routes/pages.js.map +1 -0
  268. package/dist/routes/panel.d.ts +19 -0
  269. package/dist/routes/panel.d.ts.map +1 -0
  270. package/dist/routes/panel.js +191 -0
  271. package/dist/routes/panel.js.map +1 -0
  272. package/dist/routes/relations.d.ts +21 -0
  273. package/dist/routes/relations.d.ts.map +1 -0
  274. package/dist/routes/relations.js +1239 -0
  275. package/dist/routes/relations.js.map +1 -0
  276. package/dist/routes/resources.d.ts +28 -0
  277. package/dist/routes/resources.d.ts.map +1 -0
  278. package/dist/routes/resources.js +741 -0
  279. package/dist/routes/resources.js.map +1 -0
  280. package/dist/routes/theme.d.ts +12 -0
  281. package/dist/routes/theme.d.ts.map +1 -0
  282. package/dist/routes/theme.js +82 -0
  283. package/dist/routes/theme.js.map +1 -0
  284. package/dist/routes.d.ts.map +1 -1
  285. package/dist/routes.js +64 -3078
  286. package/dist/routes.js.map +1 -1
  287. package/dist/vite.d.ts +1 -0
  288. package/dist/vite.d.ts.map +1 -1
  289. package/dist/vite.js +26 -5
  290. package/dist/vite.js.map +1 -1
  291. package/package.json +2 -1
  292. package/src/Pilotiq.ts +95 -0
  293. package/src/actions/Action.ts +79 -723
  294. package/src/actions/bulkFactories.ts +168 -0
  295. package/src/actions/crudFactories.ts +220 -0
  296. package/src/actions/factoryHelpers.ts +177 -0
  297. package/src/actions/m2mFactories.ts +193 -0
  298. package/src/actions/relationFactories.ts +372 -0
  299. package/src/elements/dispatchForm.ts +1 -1
  300. package/src/elements/dispatchTable.ts +1 -1
  301. package/src/fields/Field.ts +39 -0
  302. package/src/pageData/breadcrumbs.ts +288 -0
  303. package/src/pageData/forms.ts +578 -0
  304. package/src/pageData/helpers.ts +764 -0
  305. package/src/pageData/misc.ts +347 -0
  306. package/src/pageData/navigation.ts +779 -0
  307. package/src/pageData/relationPages.ts +1246 -0
  308. package/src/pageData/relationTabs.ts +286 -0
  309. package/src/pageData/resourcePages.ts +593 -0
  310. package/src/pageData.ts +122 -4731
  311. package/src/react/AppShell.tsx +27 -1
  312. package/src/react/CollabExtensionFactoryRegistry.ts +55 -0
  313. package/src/react/CollabRoomContext.ts +42 -0
  314. package/src/react/FormCollabBindingRegistry.ts +72 -0
  315. package/src/react/FormStateContext.tsx +91 -0
  316. package/src/react/RecordWrapperGate.tsx +40 -0
  317. package/src/react/RecordWrapperRegistry.ts +39 -0
  318. package/src/react/SchemaRenderer.tsx +230 -6479
  319. package/src/react/component-slots.test.ts +103 -0
  320. package/src/react/component-slots.ts +116 -0
  321. package/src/react/fields/BuilderInput.tsx +29 -117
  322. package/src/react/fields/MarkdownInput.tsx +0 -1
  323. package/src/react/fields/RepeaterInput.tsx +29 -130
  324. package/src/react/fields/rowState.ts +106 -0
  325. package/src/react/fields/useRowReorderDnd.ts +78 -0
  326. package/src/react/index.ts +38 -0
  327. package/src/react/layouts/SidebarLayout.tsx +39 -28
  328. package/src/react/layouts/TopbarLayout.tsx +70 -57
  329. package/src/react/parseRecordEditUrl.test.ts +75 -0
  330. package/src/react/parseRecordEditUrl.ts +55 -0
  331. package/src/react/persistedState.ts +40 -0
  332. package/src/react/schemaRenderer/AlertRenderer.tsx +112 -0
  333. package/src/react/schemaRenderer/EntryRenderer.tsx +501 -0
  334. package/src/react/schemaRenderer/SectionRenderer.tsx +120 -0
  335. package/src/react/schemaRenderer/SimpleElements.tsx +306 -0
  336. package/src/react/schemaRenderer/TabsRenderer.tsx +62 -0
  337. package/src/react/schemaRenderer/WizardRenderer.tsx +338 -0
  338. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +177 -0
  339. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +273 -0
  340. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +61 -0
  341. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +43 -0
  342. package/src/react/schemaRenderer/action/MethodActionButton.tsx +64 -0
  343. package/src/react/schemaRenderer/action/buttons.tsx +99 -0
  344. package/src/react/schemaRenderer/action/helpers.ts +140 -0
  345. package/src/react/schemaRenderer/action/renderAction.tsx +245 -0
  346. package/src/react/schemaRenderer/columnFormat.ts +65 -0
  347. package/src/react/schemaRenderer/constants.ts +50 -0
  348. package/src/react/schemaRenderer/form/FormRenderer.tsx +245 -0
  349. package/src/react/schemaRenderer/form/renderField.tsx +511 -0
  350. package/src/react/schemaRenderer/helpers.tsx +81 -0
  351. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +308 -0
  352. package/src/react/schemaRenderer/table/TableRenderer.tsx +123 -0
  353. package/src/react/schemaRenderer/table/TableRendererBody.tsx +974 -0
  354. package/src/react/schemaRenderer/table/filters.tsx +1233 -0
  355. package/src/react/schemaRenderer/table/formatCell.tsx +264 -0
  356. package/src/react/schemaRenderer/table/links.tsx +112 -0
  357. package/src/react/schemaRenderer/table/renderRowActions.tsx +52 -0
  358. package/src/react/schemaRenderer/table/url.tsx +143 -0
  359. package/src/routes/globals.ts +154 -0
  360. package/src/routes/helpers.ts +668 -0
  361. package/src/routes/pages.ts +173 -0
  362. package/src/routes/panel.ts +204 -0
  363. package/src/routes/relations.ts +1219 -0
  364. package/src/routes/resources.ts +786 -0
  365. package/src/routes/theme.ts +109 -0
  366. package/src/routes.test.ts +1 -1
  367. package/src/routes.ts +64 -3176
  368. package/src/schema/TableWidget.test.ts +2 -2
  369. package/src/theme/migrate.test.ts +178 -0
  370. package/src/vite.test.ts +184 -0
  371. package/src/vite.ts +26 -4
@@ -0,0 +1,764 @@
1
+ import type { Pilotiq, PilotiqConfig } from '../Pilotiq.js'
2
+ import type { Page } from '../Page.js'
3
+ import { Element } from '../schema/Element.js'
4
+ import { Field } from '../fields/Field.js'
5
+ import type { SchemaContext, RenderContext } from '../schema/resolveSchema.js'
6
+ import { Form } from '../elements/Form.js'
7
+ import { Table } from '../elements/Table.js'
8
+ import { Column } from '../Column.js'
9
+ import { SelectField } from '../fields/SelectField.js'
10
+ import { isRepeaterField, RepeaterField } from '../fields/RepeaterField.js'
11
+ import { isBuilderField, BuilderField } from '../fields/BuilderField.js'
12
+ import { isServerDataElement, type ServerDataElement } from '../schema/ServerDataElement.js'
13
+ import { findActions, findRowExtraActions } from '../elements/dispatchAction.js'
14
+ import { findForms, loadRelationRows } from '../elements/dispatchForm.js'
15
+ import { findTables } from '../elements/dispatchTable.js'
16
+ import {
17
+ getMorphRelationDescriptor,
18
+ getPrimaryKey,
19
+ type ModelLike,
20
+ } from '../orm/modelDefaults.js'
21
+
22
+ // ─── pageData shared helpers ────────────────────────────────
23
+ //
24
+ // URL-tag helpers (stamp dispatch URLs / cell-edit endpoints / wizard
25
+ // endpoints / mention-resolve endpoints), the load-record fill pipeline
26
+ // (`applyFillPipeline` + relationship Repeater/Builder fills),
27
+ // server-data widget resolution, and the two `*Ctx` helpers that the
28
+ // per-role builders use to construct the `SchemaContext` they pass into
29
+ // `resolveSchema`. Pure (mostly stateless) helpers — no per-page-role
30
+ // orchestration.
31
+
32
+
33
+ export function userCtx<C extends SchemaContext>(ctx: C, user: unknown): C {
34
+ if (user === null || user === undefined) return ctx
35
+ return { ...ctx, user: user as NonNullable<SchemaContext['user']> }
36
+ }
37
+
38
+ /**
39
+ * Run a (possibly async) boolean predicate; fail closed when it throws.
40
+ *
41
+ * Every page-builder pre-flight that consults `canX(user, …)` needs this
42
+ * shape — predicates may be sync booleans, Promises, or throw at the
43
+ * remote-auth layer; a throw must never propagate to the wire as an
44
+ * unhandled 500.
45
+ */
46
+ export async function safeBool(fn: () => boolean | Promise<boolean>): Promise<boolean> {
47
+ try { return Boolean(await fn()) } catch { return false }
48
+ }
49
+
50
+ /** Plan #6 — stamp the panel-wide upload URL so `FileUpload` fields
51
+ * emit it on their meta. Single URL for the whole panel; no per-field
52
+ * variation. The route is always registered (see `_uploads` in
53
+ * `routes.ts`) — meta is stamped regardless of whether an adapter is
54
+ * configured so the renderer can show a clear error rather than
55
+ * silently breaking. The companion `hasUploadAdapter` flag distinguishes
56
+ * "URL exists but adapter missing" so fields with optional upload
57
+ * affordances (e.g. `MarkdownField`'s `attachFiles` button) can hide
58
+ * themselves rather than render a broken control. */
59
+ export function uploadCtx<C extends SchemaContext>(ctx: C, cfg: PilotiqConfig): C {
60
+ return {
61
+ ...ctx,
62
+ uploadUrl: `${cfg.path}/_uploads`,
63
+ ...(cfg.uploads ? { hasUploadAdapter: true } : {}),
64
+ }
65
+ }
66
+
67
+
68
+ export async function callPageSchema(PageClass: typeof Page, ctx: SchemaContext): Promise<Element[]> {
69
+ return Promise.resolve(PageClass.schema(ctx))
70
+ }
71
+
72
+ /** Mark every Form on the page with its action URL so the rendered <form> posts to itself. */
73
+ export function tagFormActions(elements: ReadonlyArray<Element>, action: string): void {
74
+ for (const form of findForms(elements)) {
75
+ if (!form.getAction()) form.action(action)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Plan #5 — stamp the partial-resolve endpoint URL on every form whose
81
+ * descendants include at least one `live()` field. The client uses
82
+ * `FormMeta.stateUrl` to flip into controlled-state mode; forms without
83
+ * any live fields stay uncontrolled (zero-cost legacy path).
84
+ *
85
+ * `urlBuilder(formId)` lets the caller compose a per-form URL — the
86
+ * endpoint shape is `${base}/${slug}/_form/${formId}/state` so each
87
+ * form on a multi-form page gets its own route segment.
88
+ */
89
+ export function tagFormStateUrls(
90
+ elements: ReadonlyArray<Element>,
91
+ urlBuilder: (formId: string) => string,
92
+ ): void {
93
+ for (const form of findForms(elements)) {
94
+ if (formHasLiveField(form)) {
95
+ form.withStateUrl(urlBuilder(form.getFormId()))
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Reorderable rows — stamp the POST-reorder URL on every `Table` that
102
+ * has `Table.reorderable()` set. The renderer reads `TableMeta.reorderUrl`
103
+ * to wire the drop handler; tables that aren't reorderable skip wiring
104
+ * entirely. Same shape as `tagFormStateUrls` so the call site stays
105
+ * consistent.
106
+ */
107
+ export function tagTableReorderUrls(
108
+ elements: ReadonlyArray<Element>,
109
+ url: string,
110
+ ): void {
111
+ for (const table of findTables(elements)) {
112
+ if (table.isReorderable() && !table.getReorderUrl()) {
113
+ table.withReorderUrl(url)
114
+ }
115
+ }
116
+ }
117
+
118
+ // Marks every Table on the page deferred and stamps the URL the
119
+ // renderer will fetch from after mount. Must run BEFORE `loadTableRecords`
120
+ // so the records handler short-circuits.
121
+ export function tagTableDeferred(
122
+ elements: ReadonlyArray<Element>,
123
+ url: string,
124
+ ): void {
125
+ for (const table of findTables(elements)) {
126
+ table.withDeferred(true)
127
+ table.withTableUrl(url)
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Editable cell columns — walk every table on the page and stamp
133
+ * `_cellEditUrls[colName]` per row, but only on rows that already
134
+ * carry a `_cellEditable[colName]` marker (set by `loadTableRecords`
135
+ * after `R.canEdit(user, row)` passed). The dispatcher stays
136
+ * URL-shape-agnostic; URL building lives here parallel to
137
+ * `tagFormStateUrls / tagTableReorderUrls`.
138
+ *
139
+ * `idOf` extracts the per-row primary key. Defaults to reading `id` —
140
+ * works for the rudder ORM convention. Resources with a different
141
+ * primary-key column should pass an override (none in v1).
142
+ */
143
+ export function tagCellEditUrls(
144
+ elements: ReadonlyArray<Element>,
145
+ resourceUrl: string,
146
+ idOf: (row: Record<string, unknown>) => unknown = row => row['id'],
147
+ ): void {
148
+ for (const table of findTables(elements)) {
149
+ const rows = table.getRows() as ReadonlyArray<Record<string, unknown>> | undefined
150
+ if (!rows || rows.length === 0) continue
151
+ // Optimisation: skip the table when none of its columns are editable.
152
+ const editable = (table.getChildren() ?? []).some(c => c instanceof Column && c.isEditable())
153
+ if (!editable) continue
154
+ for (const row of rows) {
155
+ const editableMap = row['_cellEditable'] as Record<string, true> | undefined
156
+ if (!editableMap) continue
157
+ const id = idOf(row)
158
+ if (id === undefined || id === null || id === '') continue
159
+ const urls: Record<string, string> = {}
160
+ for (const colName of Object.keys(editableMap)) {
161
+ urls[colName] = `${resourceUrl}/${encodeURIComponent(String(id))}/_cell/${encodeURIComponent(colName)}`
162
+ }
163
+ ;(row as Record<string, unknown>)['_cellEditUrls'] = urls
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Plan #8 — stamp the wizard step-validate endpoint URL on every form
170
+ * whose descendants include a `Wizard` element. `FormMeta.wizardUrl` is
171
+ * what the client posts to on Next-button clicks; forms without a wizard
172
+ * descendant skip wiring.
173
+ */
174
+ export function tagFormWizardUrls(
175
+ elements: ReadonlyArray<Element>,
176
+ urlBuilder: (formId: string) => string,
177
+ ): void {
178
+ for (const form of findForms(elements)) {
179
+ if (formHasWizard(form)) {
180
+ form.withWizardUrl(urlBuilder(form.getFormId()))
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Stamp `_agentRunBase` on every field element in the resolved
187
+ * `ElementMeta[]` tree that carries `aiActions`. Operates on the
188
+ * post-`resolveSchema` wire shape (plain objects) rather than on
189
+ * `Element` instances — `aiActions` is added by the `field-ai.ts`
190
+ * wrapper during `toMeta()`, so it isn't visible to pre-resolve walkers.
191
+ *
192
+ * Only called on edit pages where a `recordId` is known. Create pages
193
+ * deliberately skip it — field AI actions target existing content.
194
+ */
195
+ export function tagFieldAiUrls(
196
+ elements: ReadonlyArray<Record<string, unknown>>,
197
+ agentBase: string,
198
+ ): void {
199
+ for (const el of elements) {
200
+ if (Array.isArray(el['aiActions']) && (el['aiActions'] as unknown[]).length > 0) {
201
+ ;(el as Record<string, unknown>)['_agentRunBase'] = agentBase
202
+ }
203
+ const children = el['children']
204
+ if (Array.isArray(children)) tagFieldAiUrls(children as Record<string, unknown>[], agentBase)
205
+ // Repeater rows
206
+ const rows = el['rows']
207
+ if (Array.isArray(rows)) {
208
+ for (const row of rows as Record<string, unknown>[]) {
209
+ const rowChildren = row['children']
210
+ if (Array.isArray(rowChildren)) tagFieldAiUrls(rowChildren as Record<string, unknown>[], agentBase)
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Audit row 2026-05-07 cont'd⁸ — stamp the inline-create-option endpoint
218
+ * URL on every `SelectField` that has called `createOptionForm()`. Walks
219
+ * every form on the page so the URL carries the parent form's id; URL
220
+ * shape `${formScopeUrl}/_form/${formId}/create-option/${fieldName}` so
221
+ * the route handler can pick the form by id and the field by name.
222
+ *
223
+ * Mirrors `tagFormStateUrls / tagFormWizardUrls` — operates on the
224
+ * un-resolved Element tree, mutates field-instance state via
225
+ * `field.withCreateOptionUrl(url)`, and the field's `toMeta()` reads it
226
+ * back to emit `createOption.url`.
227
+ *
228
+ * Stops at Repeater / Builder boundaries (parallel to the form-state /
229
+ * wizard walkers): inside-row schemas are dispatched per-row and the
230
+ * createOption shape doesn't compose with row body coercion in v1.
231
+ */
232
+ export function tagSelectCreateOptionUrls(
233
+ elements: ReadonlyArray<Element>,
234
+ urlBuilder: (formId: string, fieldName: string) => string,
235
+ ): void {
236
+ for (const form of findForms(elements)) {
237
+ const formId = form.getFormId()
238
+ walkSelectFields(form.getChildren() as Element[] ?? [], (field) => {
239
+ if (field.hasCreateOption() && !field.getCreateOptionUrl()) {
240
+ field.withCreateOptionUrl(urlBuilder(formId, field.name))
241
+ }
242
+ })
243
+ }
244
+ }
245
+
246
+ export function walkSelectFields(elements: Element[], visit: (f: SelectField) => void): void {
247
+ for (const el of elements) {
248
+ if (el instanceof SelectField) {
249
+ visit(el)
250
+ // SelectField has no children of its own — no recursion needed.
251
+ continue
252
+ }
253
+ // Stop at row-array boundaries — see comment on `tagSelectCreateOptionUrls`.
254
+ if (el instanceof RepeaterField) continue
255
+ if (el instanceof BuilderField) continue
256
+ const children = el.getChildren()
257
+ if (children && children.length > 0) walkSelectFields(children as Element[], visit)
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Adapter-package async-resolve walker. Stamps the per-form mentions URL
263
+ * on every field that ducks like a "rich text with at least one async
264
+ * mention provider". The duck-typed contract lives here (as opposed to
265
+ * importing from `@pilotiq/tiptap`) so pilotiq core stays adapter-free —
266
+ * any future field type with an async-resolve trigger can satisfy the
267
+ * same shape and pick up URL stamping for free.
268
+ *
269
+ * Contract:
270
+ * - `getType() === 'richtext'` (fast filter)
271
+ * - `hasAsyncMentions(): boolean`
272
+ * - `withMentionsUrl(url: string): unknown`
273
+ *
274
+ * Walks every form on the page so the URL builder can mint a per-form
275
+ * URL (mirrors `tagFormStateUrls / tagFormWizardUrls`). The route handler
276
+ * uses formId in the URL to select the form; the body carries `field`
277
+ * + `trigger` + `query`. One URL per (form, scope), reused across every
278
+ * async-mention field on that form.
279
+ */
280
+ interface AsyncMentionFieldLike {
281
+ hasAsyncMentions(): boolean
282
+ withMentionsUrl(url: string): unknown
283
+ }
284
+
285
+ export function isAsyncMentionField(el: Element): el is Element & AsyncMentionFieldLike {
286
+ if (el.getType() !== 'richtext') return false
287
+ const candidate = el as unknown as Partial<AsyncMentionFieldLike>
288
+ return typeof candidate.hasAsyncMentions === 'function'
289
+ && typeof candidate.withMentionsUrl === 'function'
290
+ }
291
+
292
+ export function tagRichTextMentionUrls(
293
+ elements: ReadonlyArray<Element>,
294
+ urlBuilder: (formId: string) => string,
295
+ ): void {
296
+ for (const form of findForms(elements)) {
297
+ const url = urlBuilder(form.getFormId())
298
+ const visit = (els: ReadonlyArray<Element>): void => {
299
+ for (const el of els) {
300
+ // Don't cross into nested forms — each form gets its own URL.
301
+ if (el !== form && el.getType() === 'form') continue
302
+ if (isAsyncMentionField(el) && el.hasAsyncMentions()) {
303
+ el.withMentionsUrl(url)
304
+ }
305
+ // Builder.getChildren() returns undefined to keep the field-level
306
+ // walkers from treating heterogeneous rows as flat children. Manual
307
+ // descent into each block's schema covers the URL-stamping path
308
+ // without changing the no-cross posture for save/coerce.
309
+ if (isBuilderField(el)) {
310
+ for (const block of (el as BuilderField).getBlocks()) visit(block.getSchema())
311
+ continue
312
+ }
313
+ const children = el.getChildren()
314
+ if (children) visit(children)
315
+ }
316
+ }
317
+ const children = form.getChildren()
318
+ if (children) visit(children)
319
+ }
320
+ }
321
+
322
+ export function formHasLiveField(form: Form): boolean {
323
+ let found = false
324
+ const visit = (els: ReadonlyArray<Element>): void => {
325
+ for (const el of els) {
326
+ if (found) return
327
+ // Either a server-side `live()` (drives a roundtrip) OR a
328
+ // client-side `afterStateUpdatedJs(body)` (JS-only) is enough to
329
+ // mount the controlled-form path: the FormStateProvider holds the
330
+ // values map either path needs, and the client gates the actual
331
+ // network POST on `live` separately. Cost of the over-stamp for
332
+ // JS-only forms is one unused endpoint URL per form — endpoint
333
+ // never gets hit because the client only POSTs on `live`.
334
+ if (el instanceof Field && (el.isLive() || el.getAfterStateUpdatedJs() !== undefined)) {
335
+ found = true
336
+ return
337
+ }
338
+ const children = el.getChildren()
339
+ if (children) visit(children)
340
+ }
341
+ }
342
+ const children = form.getChildren()
343
+ if (children) visit(children)
344
+ return found
345
+ }
346
+
347
+ export function formHasWizard(form: Form): boolean {
348
+ let found = false
349
+ const visit = (els: ReadonlyArray<Element>): void => {
350
+ for (const el of els) {
351
+ if (found) return
352
+ if (el.getType() === 'wizard') { found = true; return }
353
+ const children = el.getChildren()
354
+ if (children) visit(children)
355
+ }
356
+ }
357
+ const children = form.getChildren()
358
+ if (children) visit(children)
359
+ return found
360
+ }
361
+
362
+ /**
363
+ * Run the edit-mode fill pipeline on a loaded record:
364
+ * mutateFormDataBeforeFill → fillFromRecord → mutateFormDataAfterFill
365
+ *
366
+ * `fillFromRecord` defaults to `{ ...record }` when not configured. Both
367
+ * mutators are optional and may be async. `ctx.record` is the loaded
368
+ * record so mutators can read from fields the form doesn't surface.
369
+ */
370
+ export async function applyFillPipeline<R>(
371
+ form: Form<R>,
372
+ record: R,
373
+ ): Promise<Record<string, unknown>> {
374
+ const recordObj = record as unknown as Record<string, unknown>
375
+ let values: Record<string, unknown> = { ...recordObj }
376
+
377
+ const before = form.getMutateFormDataBeforeFill()
378
+ if (before) values = await before(values, { values, record })
379
+
380
+ const fill = form.getFillFromRecord()
381
+ if (fill) values = fill(record)
382
+
383
+ const after = form.getMutateFormDataAfterFill()
384
+ if (after) values = await after(values, { values, record })
385
+
386
+ return values
387
+ }
388
+
389
+ /**
390
+ * Walk the form's top-level Repeaters and replace `values[fieldName]`
391
+ * with rows fetched from `parent.related(name)` for any
392
+ * relationship-backed Repeater. Each loaded row stamps `__id` to the
393
+ * child's primary key so the renderer can round-trip identity through
394
+ * a hidden input and the save-side diff can match submitted rows back
395
+ * to existing records.
396
+ *
397
+ * No-op when the parent record is null (create mode), when no
398
+ * relationship-backed Repeaters exist on the form, or when the
399
+ * resource has no `R.model` (relation queries need it).
400
+ *
401
+ * Mutates and returns a fresh values object — never the input.
402
+ */
403
+ export async function applyRelationshipRepeaterFill(
404
+ form: Form,
405
+ values: Record<string, unknown>,
406
+ record: unknown,
407
+ parentModel: ModelLike | undefined,
408
+ ): Promise<Record<string, unknown>> {
409
+ if (record == null) return values
410
+ if (!parentModel) return values
411
+ const repeaters = findRelationshipRepeaters(form.getChildren() ?? [])
412
+ if (repeaters.length === 0) return values
413
+
414
+ const out: Record<string, unknown> = { ...values }
415
+ for (const repeater of repeaters) {
416
+ const cfg = repeater.getRelationship()!
417
+ const pivotColumns = cfg.pivotColumns
418
+ let rows: unknown[]
419
+ try {
420
+ rows = await loadRelationRows(parentModel, record, cfg.name, pivotColumns)
421
+ } catch {
422
+ // Failed lookup (e.g. missing `relations` map on a test stub)
423
+ // — fall back to whatever value applyFillPipeline produced
424
+ // rather than wiping the field. Better to render stale data
425
+ // than to silently empty the row list.
426
+ continue
427
+ }
428
+
429
+ // The child model is opaque here — we don't have the full
430
+ // descriptor at this seam, so use the configured override or
431
+ // peek the parent's relations map for the FK column. Strip it
432
+ // (and the PK) from each row's payload so the inner schema
433
+ // doesn't surface them as form values. For morphMany the
434
+ // attachment is two columns instead of one — strip both.
435
+ const pkColumn = pickChildPrimaryKey(parentModel, cfg.name) ?? 'id'
436
+ const fkColumn = cfg.foreignKey ?? pickChildForeignKey(parentModel, cfg.name)
437
+ const morph = getMorphRelationDescriptor(parentModel, cfg.name)
438
+ const morphIdCol = morph ? `${morph.morphName}Id` : undefined
439
+ const morphTyCol = morph ? `${morph.morphName}Type` : undefined
440
+
441
+ out[repeater.name] = rows.map(row => {
442
+ const r = (row && typeof row === 'object') ? { ...(row as Record<string, unknown>) } : {}
443
+ const pkValue = r[pkColumn]
444
+ delete r[pkColumn]
445
+ if (fkColumn) delete r[fkColumn]
446
+ if (morphIdCol) delete r[morphIdCol]
447
+ if (morphTyCol) delete r[morphTyCol]
448
+ // M2M pivot extras — flatten `row.pivot[col]` onto the row's data
449
+ // so each pivot column round-trips through the inner schema as a
450
+ // regular form field. The pivot envelope itself is dropped from
451
+ // the values shape — the persist side splits pivot vs child
452
+ // columns by name lookup against `cfg.pivotColumns`.
453
+ const pivotEnvelope = r['pivot']
454
+ delete r['pivot']
455
+ const stamped: Record<string, unknown> = { ...r }
456
+ if (pivotColumns && pivotColumns.length > 0
457
+ && pivotEnvelope && typeof pivotEnvelope === 'object'
458
+ ) {
459
+ const pe = pivotEnvelope as Record<string, unknown>
460
+ for (const col of pivotColumns) {
461
+ if (col in pe) stamped[col] = pe[col]
462
+ }
463
+ }
464
+ if (pkValue !== undefined && pkValue !== null) {
465
+ stamped['__id'] = String(pkValue)
466
+ }
467
+ return stamped
468
+ })
469
+ }
470
+ return out
471
+ }
472
+
473
+ /** Walk the form's children for top-level relationship-backed Repeaters. */
474
+ export function findRelationshipRepeaters(elements: ReadonlyArray<Element>): RepeaterField[] {
475
+ const out: RepeaterField[] = []
476
+ const walk = (els: ReadonlyArray<Element>): void => {
477
+ for (const el of els) {
478
+ if (isRepeaterField(el)) {
479
+ const r = el as RepeaterField
480
+ if (r.getRelationship()) out.push(r)
481
+ // Don't dive into Repeater children — relationship-on-relationship
482
+ // isn't supported in v1.
483
+ continue
484
+ }
485
+ // Don't dive into Builder children either — relationship-backed
486
+ // Builders are resolved separately by `findRelationshipBuilders`.
487
+ if (isBuilderField(el)) continue
488
+ const children = el.getChildren()
489
+ if (children && children.length > 0) walk(children)
490
+ }
491
+ }
492
+ walk(elements)
493
+ return out
494
+ }
495
+
496
+ /**
497
+ * Walk the form's top-level Builders and replace `values[fieldName]` with
498
+ * rows fetched from `parent.related(name)` for any relationship-backed
499
+ * Builder. Each loaded row stamps `__id` (child PK) + `type` (block
500
+ * discriminator) + `data` (per-block JSON payload) so the renderer can
501
+ * round-trip the heterogeneous envelope.
502
+ *
503
+ * Mirrors `applyRelationshipRepeaterFill`. No-op when the parent record
504
+ * is null (create mode), the resource has no `R.model`, or no
505
+ * relationship-backed Builders exist on the form.
506
+ */
507
+ export async function applyRelationshipBuilderFill(
508
+ form: Form,
509
+ values: Record<string, unknown>,
510
+ record: unknown,
511
+ parentModel: ModelLike | undefined,
512
+ ): Promise<Record<string, unknown>> {
513
+ if (record == null) return values
514
+ if (!parentModel) return values
515
+ const builders = findRelationshipBuilders(form.getChildren() ?? [])
516
+ if (builders.length === 0) return values
517
+
518
+ const out: Record<string, unknown> = { ...values }
519
+ for (const builder of builders) {
520
+ const cfg = builder.getRelationship()!
521
+ let rows: unknown[]
522
+ try {
523
+ rows = await loadRelationRows(parentModel, record, cfg.name)
524
+ } catch {
525
+ // Failed lookup (e.g. missing `relations` map on a test stub) —
526
+ // fall back to whatever value applyFillPipeline produced rather
527
+ // than wiping the field. Better stale than silently empty.
528
+ continue
529
+ }
530
+
531
+ const pkColumn = pickChildPrimaryKey(parentModel, cfg.name) ?? 'id'
532
+ const fkColumn = cfg.foreignKey ?? pickChildForeignKey(parentModel, cfg.name)
533
+ const typeColumn = cfg.typeColumn ?? 'type'
534
+ const dataColumn = cfg.dataColumn ?? 'data'
535
+
536
+ out[builder.name] = rows.map(row => {
537
+ const r = (row && typeof row === 'object') ? { ...(row as Record<string, unknown>) } : {}
538
+ const pkValue = r[pkColumn]
539
+ const blockType = typeof r[typeColumn] === 'string' ? (r[typeColumn] as string) : ''
540
+ const dataRaw = r[dataColumn]
541
+ const blockData = parseBuilderDataPayload(dataRaw)
542
+
543
+ const stamped: Record<string, unknown> = {
544
+ type: blockType,
545
+ data: blockData,
546
+ }
547
+ if (pkValue !== undefined && pkValue !== null) {
548
+ stamped['__id'] = String(pkValue)
549
+ }
550
+ // Non-`type` / `data` / FK / PK columns aren't surfaced — the
551
+ // JSON envelope is the source of truth for per-block fields. If
552
+ // a user denormalizes a column, they handle it via per-block
553
+ // mutate hooks, not by leaking the column into row values.
554
+ void fkColumn
555
+ return stamped
556
+ })
557
+ }
558
+ return out
559
+ }
560
+
561
+ /**
562
+ * Normalize the JSON payload column into a plain object. Prisma
563
+ * hydrates `Json` columns to objects; some adapters return strings.
564
+ * Anything that isn't a parseable object falls back to `{}` so the
565
+ * inner schema renders fresh defaults.
566
+ */
567
+ export function parseBuilderDataPayload(raw: unknown): Record<string, unknown> {
568
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
569
+ return raw as Record<string, unknown>
570
+ }
571
+ if (typeof raw === 'string') {
572
+ try {
573
+ const parsed: unknown = JSON.parse(raw)
574
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
575
+ return parsed as Record<string, unknown>
576
+ }
577
+ } catch {
578
+ // fall through to {}
579
+ }
580
+ }
581
+ return {}
582
+ }
583
+
584
+ /** Walk the form's children for top-level relationship-backed Builders. */
585
+ export function findRelationshipBuilders(elements: ReadonlyArray<Element>): BuilderField[] {
586
+ const out: BuilderField[] = []
587
+ const walk = (els: ReadonlyArray<Element>): void => {
588
+ for (const el of els) {
589
+ if (isBuilderField(el)) {
590
+ const b = el as BuilderField
591
+ if (b.getRelationship()) out.push(b)
592
+ continue
593
+ }
594
+ // Don't dive into Repeater children either — both array-row
595
+ // boundaries are walker stops here.
596
+ if (isRepeaterField(el)) continue
597
+ const children = el.getChildren()
598
+ if (children && children.length > 0) walk(children)
599
+ }
600
+ }
601
+ walk(elements)
602
+ return out
603
+ }
604
+
605
+ /** Read the child model's PK column from the parent's relations map, when present. */
606
+ export function pickChildPrimaryKey(parentModel: ModelLike, name: string): string | undefined {
607
+ const relations = (parentModel as unknown as Record<string, unknown>)['relations']
608
+ if (!relations || typeof relations !== 'object') return undefined
609
+ const entry = (relations as Record<string, unknown>)[name]
610
+ if (!entry || typeof entry !== 'object') return undefined
611
+ const e = entry as Record<string, unknown>
612
+ if (typeof e['model'] !== 'function') return undefined
613
+ try {
614
+ const child = (e['model'] as () => ModelLike)()
615
+ return getPrimaryKey(child)
616
+ } catch {
617
+ return undefined
618
+ }
619
+ }
620
+
621
+ /** Read the FK column from the parent's relations map, when present. */
622
+ export function pickChildForeignKey(parentModel: ModelLike, name: string): string | undefined {
623
+ const relations = (parentModel as unknown as Record<string, unknown>)['relations']
624
+ if (!relations || typeof relations !== 'object') return undefined
625
+ const entry = (relations as Record<string, unknown>)[name]
626
+ if (!entry || typeof entry !== 'object') return undefined
627
+ const e = entry as Record<string, unknown>
628
+ return typeof e['foreignKey'] === 'string' ? (e['foreignKey'] as string) : undefined
629
+ }
630
+
631
+ // ─── Plan #15 server-data widgets ─────────────────────────────
632
+
633
+ /** Wire-shape of the per-widget data map shipped to the client.
634
+ * Lazy elements stamp `null` (renderer paints skeleton + fetches);
635
+ * eager elements stamp their resolved payload. Errors stamp
636
+ * `{ error: '<message>' }` so the renderer can surface a per-widget
637
+ * failure without blanking the page. */
638
+ export type ServerDataMap = Record<string, unknown>
639
+
640
+ /**
641
+ * Plan #15 — collect every `ServerDataElement` in the schema tree and
642
+ * resolve their `getServerData(ctx)` payloads in parallel. Returns a
643
+ * map keyed by element id, ready to ship as `viewProps._widgetData`.
644
+ *
645
+ * Lazy elements (default — `lazy(false)` opts out) skip the hook and
646
+ * stamp `null` so the renderer paints a skeleton and fetches the
647
+ * payload via `POST {base}/_widget/:id` on mount. Eager elements
648
+ * resolve synchronously and ship the data with the page.
649
+ *
650
+ * Per-widget errors are caught and surfaced as `{ error: '...' }` —
651
+ * one flaky `getStats()` shouldn't 500 the entire dashboard.
652
+ *
653
+ * Visibility is **not** re-evaluated here. The schema resolver
654
+ * (`resolveSchema → evaluateVisibility`) drops hidden layout elements
655
+ * before any widget code runs. Widgets inside still-rendered branches
656
+ * always resolve (or stamp lazy null).
657
+ */
658
+ export async function resolveServerDataElements(
659
+ elements: ReadonlyArray<Element>,
660
+ ctx: RenderContext,
661
+ ): Promise<ServerDataMap> {
662
+ const widgets = collectServerDataElements(elements)
663
+ if (widgets.length === 0) return {}
664
+ const out: ServerDataMap = {}
665
+ await Promise.all(widgets.map(async (el) => {
666
+ const id = el.getId()
667
+ if (el.isLazy()) {
668
+ out[id] = null // sentinel — renderer paints skeleton, fetches on mount
669
+ return
670
+ }
671
+ try {
672
+ out[id] = await el.resolveServerData(ctx)
673
+ } catch (err) {
674
+ out[id] = { error: err instanceof Error ? err.message : 'Widget failed to load' }
675
+ }
676
+ }))
677
+ return out
678
+ }
679
+
680
+ /** Walk the tree collecting every `ServerDataElement`. Walks into
681
+ * containers but stops at Form/Repeater/Builder boundaries — widgets
682
+ * inside an editable form don't make sense in v1. */
683
+ export function collectServerDataElements(elements: ReadonlyArray<Element>): ServerDataElement[] {
684
+ const out: ServerDataElement[] = []
685
+ const walk = (els: ReadonlyArray<Element>): void => {
686
+ for (const el of els) {
687
+ if (isServerDataElement(el)) {
688
+ out.push(el)
689
+ // Don't recurse into a widget's children — `View` etc. are leaves
690
+ // for v1 (no nested widgets inside widgets).
691
+ continue
692
+ }
693
+ // Skip walkers that imply per-row resolution — widgets inside
694
+ // Repeater/Builder rows don't have a stable id space.
695
+ const type = el.getType()
696
+ if (type === 'form' || type === 'repeater' || type === 'builder' || type === 'table' || type === 'tableWidget') continue
697
+ const children = el.getChildren()
698
+ if (children) walk(children)
699
+ }
700
+ }
701
+ walk(elements)
702
+ return out
703
+ }
704
+
705
+ /**
706
+ * Plan #15 — stamp the polling-endpoint URL on every `ServerDataElement`
707
+ * in the tree. Mirrors `tagFormStateUrls / tagTableReorderUrls`. Walks
708
+ * with the same boundaries as `collectServerDataElements` so the wire
709
+ * stays in sync (no orphan widgets without URLs and vice versa).
710
+ *
711
+ * `urlBuilder(id)` typically produces `${base}/_widget/${id}` for
712
+ * dashboard widgets and `${base}/${pageSlug}/_widget/${id}` for
713
+ * custom-page widgets — the route handlers for both shapes are wired up
714
+ * in `routes.ts` (see Phase A.4).
715
+ */
716
+ export function tagWidgetUrls(
717
+ elements: ReadonlyArray<Element>,
718
+ urlBuilder: (id: string) => string,
719
+ ): void {
720
+ for (const widget of collectServerDataElements(elements)) {
721
+ if (widget.getWidgetUrl()) continue // user-set wins
722
+ widget.withWidgetUrl(urlBuilder(widget.getId()))
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Stamp every form-subresource URL the page might need in one pass:
728
+ * - `_form/${formId}/state` (Plan #5 partial-resolve)
729
+ * - `_form/${formId}/wizard` (Plan #8 step-validate)
730
+ * - `_form/${formId}/mentions` (async-mention typeahead)
731
+ * - `_form/${formId}/create-option/${fieldName}` (SelectField inline-create)
732
+ *
733
+ * Every page-builder that mounts a form (dashboard / resource create+edit /
734
+ * global edit / custom page) needs all four — the underlying taggers skip
735
+ * forms that don't carry the matching feature, so this is always safe to
736
+ * call. `base` is the route prefix (e.g. `${cfg.path}` / `${resourceBase}` /
737
+ * `${resourceBase}/${recordId}` / `${pageUrl}`).
738
+ */
739
+ export function tagFormSubresourceUrls(elements: ReadonlyArray<Element>, base: string): void {
740
+ tagFormStateUrls(elements, formId => `${base}/_form/${formId}/state`)
741
+ tagFormWizardUrls(elements, formId => `${base}/_form/${formId}/wizard`)
742
+ tagRichTextMentionUrls(elements, formId => `${base}/_form/${formId}/mentions`)
743
+ tagSelectCreateOptionUrls(elements, (formId, fieldName) => `${base}/_form/${formId}/create-option/${fieldName}`)
744
+ }
745
+
746
+ /** Stamp dispatchUrl on every handler-style Action so the client knows where to POST. */
747
+ export function tagActionDispatch(elements: ReadonlyArray<Element>, baseUrl: string): void {
748
+ for (const action of findActions(elements)) {
749
+ if (!action.getHandler()) continue
750
+ if (action.getHref() || action.getMethod()) continue
751
+ if (action.getDispatchUrl()) continue
752
+ action.dispatchUrl(`${baseUrl}/_action/${action.name}`)
753
+ }
754
+ // Row-scoped extraItemActions (Repeater/Builder). Stamped here too so
755
+ // the client can POST to the same `_action/:name` route — the renderer
756
+ // attaches `_rowPath=<fieldName>.<index>` per click; the server's
757
+ // dispatcher uses that to walk into the right row when building
758
+ // `ctx.row`. See `findRowExtraActions` in `dispatchAction.ts`.
759
+ for (const { action } of findRowExtraActions(elements)) {
760
+ if (!action.getHandler()) continue
761
+ if (action.getDispatchUrl()) continue
762
+ action.dispatchUrl(`${baseUrl}/_action/${action.name}`)
763
+ }
764
+ }