@pilotiq/pilotiq 0.7.1 → 0.8.0

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 (367) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +154 -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/RecordWrapperGate.d.ts +25 -0
  93. package/dist/react/RecordWrapperGate.d.ts.map +1 -0
  94. package/dist/react/RecordWrapperGate.js +30 -0
  95. package/dist/react/RecordWrapperGate.js.map +1 -0
  96. package/dist/react/RecordWrapperRegistry.d.ts +31 -0
  97. package/dist/react/RecordWrapperRegistry.d.ts.map +1 -0
  98. package/dist/react/RecordWrapperRegistry.js +15 -0
  99. package/dist/react/RecordWrapperRegistry.js.map +1 -0
  100. package/dist/react/SchemaRenderer.d.ts +17 -23
  101. package/dist/react/SchemaRenderer.d.ts.map +1 -1
  102. package/dist/react/SchemaRenderer.js +71 -3647
  103. package/dist/react/SchemaRenderer.js.map +1 -1
  104. package/dist/react/component-slots.d.ts +103 -0
  105. package/dist/react/component-slots.d.ts.map +1 -0
  106. package/dist/react/component-slots.js +18 -0
  107. package/dist/react/component-slots.js.map +1 -0
  108. package/dist/react/fields/BuilderInput.d.ts.map +1 -1
  109. package/dist/react/fields/BuilderInput.js +21 -117
  110. package/dist/react/fields/BuilderInput.js.map +1 -1
  111. package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
  112. package/dist/react/fields/MarkdownInput.js +1 -3
  113. package/dist/react/fields/MarkdownInput.js.map +1 -1
  114. package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
  115. package/dist/react/fields/RepeaterInput.js +22 -127
  116. package/dist/react/fields/RepeaterInput.js.map +1 -1
  117. package/dist/react/fields/rowState.d.ts +40 -0
  118. package/dist/react/fields/rowState.d.ts.map +1 -0
  119. package/dist/react/fields/rowState.js +60 -0
  120. package/dist/react/fields/rowState.js.map +1 -0
  121. package/dist/react/fields/useRowReorderDnd.d.ts +28 -0
  122. package/dist/react/fields/useRowReorderDnd.d.ts.map +1 -0
  123. package/dist/react/fields/useRowReorderDnd.js +51 -0
  124. package/dist/react/fields/useRowReorderDnd.js.map +1 -0
  125. package/dist/react/index.d.ts +9 -0
  126. package/dist/react/index.d.ts.map +1 -1
  127. package/dist/react/index.js +8 -0
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/react/layouts/SidebarLayout.d.ts +1 -1
  130. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  131. package/dist/react/layouts/SidebarLayout.js +10 -2
  132. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  133. package/dist/react/layouts/TopbarLayout.d.ts +1 -1
  134. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -1
  135. package/dist/react/layouts/TopbarLayout.js +19 -11
  136. package/dist/react/layouts/TopbarLayout.js.map +1 -1
  137. package/dist/react/parseRecordEditUrl.d.ts +29 -0
  138. package/dist/react/parseRecordEditUrl.d.ts.map +1 -0
  139. package/dist/react/parseRecordEditUrl.js +25 -0
  140. package/dist/react/parseRecordEditUrl.js.map +1 -0
  141. package/dist/react/persistedState.d.ts +19 -0
  142. package/dist/react/persistedState.d.ts.map +1 -0
  143. package/dist/react/persistedState.js +51 -0
  144. package/dist/react/persistedState.js.map +1 -0
  145. package/dist/react/schemaRenderer/AlertRenderer.d.ts +12 -0
  146. package/dist/react/schemaRenderer/AlertRenderer.d.ts.map +1 -0
  147. package/dist/react/schemaRenderer/AlertRenderer.js +61 -0
  148. package/dist/react/schemaRenderer/AlertRenderer.js.map +1 -0
  149. package/dist/react/schemaRenderer/EntryRenderer.d.ts +13 -0
  150. package/dist/react/schemaRenderer/EntryRenderer.d.ts.map +1 -0
  151. package/dist/react/schemaRenderer/EntryRenderer.js +277 -0
  152. package/dist/react/schemaRenderer/EntryRenderer.js.map +1 -0
  153. package/dist/react/schemaRenderer/SectionRenderer.d.ts +16 -0
  154. package/dist/react/schemaRenderer/SectionRenderer.d.ts.map +1 -0
  155. package/dist/react/schemaRenderer/SectionRenderer.js +62 -0
  156. package/dist/react/schemaRenderer/SectionRenderer.js.map +1 -0
  157. package/dist/react/schemaRenderer/SimpleElements.d.ts +25 -0
  158. package/dist/react/schemaRenderer/SimpleElements.d.ts.map +1 -0
  159. package/dist/react/schemaRenderer/SimpleElements.js +147 -0
  160. package/dist/react/schemaRenderer/SimpleElements.js.map +1 -0
  161. package/dist/react/schemaRenderer/TabsRenderer.d.ts +17 -0
  162. package/dist/react/schemaRenderer/TabsRenderer.d.ts.map +1 -0
  163. package/dist/react/schemaRenderer/TabsRenderer.js +31 -0
  164. package/dist/react/schemaRenderer/TabsRenderer.js.map +1 -0
  165. package/dist/react/schemaRenderer/WizardRenderer.d.ts +34 -0
  166. package/dist/react/schemaRenderer/WizardRenderer.d.ts.map +1 -0
  167. package/dist/react/schemaRenderer/WizardRenderer.js +208 -0
  168. package/dist/react/schemaRenderer/WizardRenderer.js.map +1 -0
  169. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts +21 -0
  170. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts.map +1 -0
  171. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js +82 -0
  172. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js.map +1 -0
  173. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts +30 -0
  174. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts.map +1 -0
  175. package/dist/react/schemaRenderer/action/ActionModalDialog.js +182 -0
  176. package/dist/react/schemaRenderer/action/ActionModalDialog.js.map +1 -0
  177. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts +17 -0
  178. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts.map +1 -0
  179. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js +19 -0
  180. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js.map +1 -0
  181. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts +16 -0
  182. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts.map +1 -0
  183. package/dist/react/schemaRenderer/action/HandlerActionButton.js +16 -0
  184. package/dist/react/schemaRenderer/action/HandlerActionButton.js.map +1 -0
  185. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts +22 -0
  186. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts.map +1 -0
  187. package/dist/react/schemaRenderer/action/MethodActionButton.js +26 -0
  188. package/dist/react/schemaRenderer/action/MethodActionButton.js.map +1 -0
  189. package/dist/react/schemaRenderer/action/buttons.d.ts +18 -0
  190. package/dist/react/schemaRenderer/action/buttons.d.ts.map +1 -0
  191. package/dist/react/schemaRenderer/action/buttons.js +74 -0
  192. package/dist/react/schemaRenderer/action/buttons.js.map +1 -0
  193. package/dist/react/schemaRenderer/action/helpers.d.ts +26 -0
  194. package/dist/react/schemaRenderer/action/helpers.d.ts.map +1 -0
  195. package/dist/react/schemaRenderer/action/helpers.js +126 -0
  196. package/dist/react/schemaRenderer/action/helpers.js.map +1 -0
  197. package/dist/react/schemaRenderer/action/renderAction.d.ts +21 -0
  198. package/dist/react/schemaRenderer/action/renderAction.d.ts.map +1 -0
  199. package/dist/react/schemaRenderer/action/renderAction.js +102 -0
  200. package/dist/react/schemaRenderer/action/renderAction.js.map +1 -0
  201. package/dist/react/schemaRenderer/columnFormat.d.ts +10 -0
  202. package/dist/react/schemaRenderer/columnFormat.d.ts.map +1 -0
  203. package/dist/react/schemaRenderer/columnFormat.js +76 -0
  204. package/dist/react/schemaRenderer/columnFormat.js.map +1 -0
  205. package/dist/react/schemaRenderer/constants.d.ts +8 -0
  206. package/dist/react/schemaRenderer/constants.d.ts.map +1 -0
  207. package/dist/react/schemaRenderer/constants.js +45 -0
  208. package/dist/react/schemaRenderer/constants.js.map +1 -0
  209. package/dist/react/schemaRenderer/form/FormRenderer.d.ts +29 -0
  210. package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -0
  211. package/dist/react/schemaRenderer/form/FormRenderer.js +152 -0
  212. package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -0
  213. package/dist/react/schemaRenderer/form/renderField.d.ts +6 -0
  214. package/dist/react/schemaRenderer/form/renderField.d.ts.map +1 -0
  215. package/dist/react/schemaRenderer/form/renderField.js +239 -0
  216. package/dist/react/schemaRenderer/form/renderField.js.map +1 -0
  217. package/dist/react/schemaRenderer/helpers.d.ts +32 -0
  218. package/dist/react/schemaRenderer/helpers.d.ts.map +1 -0
  219. package/dist/react/schemaRenderer/helpers.js +52 -0
  220. package/dist/react/schemaRenderer/helpers.js.map +1 -0
  221. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts +60 -0
  222. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts.map +1 -0
  223. package/dist/react/schemaRenderer/table/CardsLayoutBody.js +189 -0
  224. package/dist/react/schemaRenderer/table/CardsLayoutBody.js.map +1 -0
  225. package/dist/react/schemaRenderer/table/TableRenderer.d.ts +29 -0
  226. package/dist/react/schemaRenderer/table/TableRenderer.d.ts.map +1 -0
  227. package/dist/react/schemaRenderer/table/TableRenderer.js +85 -0
  228. package/dist/react/schemaRenderer/table/TableRenderer.js.map +1 -0
  229. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts +18 -0
  230. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts.map +1 -0
  231. package/dist/react/schemaRenderer/table/TableRendererBody.js +555 -0
  232. package/dist/react/schemaRenderer/table/TableRendererBody.js.map +1 -0
  233. package/dist/react/schemaRenderer/table/filters.d.ts +263 -0
  234. package/dist/react/schemaRenderer/table/filters.d.ts.map +1 -0
  235. package/dist/react/schemaRenderer/table/filters.js +497 -0
  236. package/dist/react/schemaRenderer/table/filters.js.map +1 -0
  237. package/dist/react/schemaRenderer/table/formatCell.d.ts +11 -0
  238. package/dist/react/schemaRenderer/table/formatCell.d.ts.map +1 -0
  239. package/dist/react/schemaRenderer/table/formatCell.js +172 -0
  240. package/dist/react/schemaRenderer/table/formatCell.js.map +1 -0
  241. package/dist/react/schemaRenderer/table/links.d.ts +42 -0
  242. package/dist/react/schemaRenderer/table/links.d.ts.map +1 -0
  243. package/dist/react/schemaRenderer/table/links.js +55 -0
  244. package/dist/react/schemaRenderer/table/links.js.map +1 -0
  245. package/dist/react/schemaRenderer/table/renderRowActions.d.ts +13 -0
  246. package/dist/react/schemaRenderer/table/renderRowActions.d.ts.map +1 -0
  247. package/dist/react/schemaRenderer/table/renderRowActions.js +25 -0
  248. package/dist/react/schemaRenderer/table/renderRowActions.js.map +1 -0
  249. package/dist/react/schemaRenderer/table/url.d.ts +41 -0
  250. package/dist/react/schemaRenderer/table/url.d.ts.map +1 -0
  251. package/dist/react/schemaRenderer/table/url.js +114 -0
  252. package/dist/react/schemaRenderer/table/url.js.map +1 -0
  253. package/dist/routes/globals.d.ts +13 -0
  254. package/dist/routes/globals.d.ts.map +1 -0
  255. package/dist/routes/globals.js +131 -0
  256. package/dist/routes/globals.js.map +1 -0
  257. package/dist/routes/helpers.d.ts +217 -0
  258. package/dist/routes/helpers.d.ts.map +1 -0
  259. package/dist/routes/helpers.js +498 -0
  260. package/dist/routes/helpers.js.map +1 -0
  261. package/dist/routes/pages.d.ts +15 -0
  262. package/dist/routes/pages.d.ts.map +1 -0
  263. package/dist/routes/pages.js +145 -0
  264. package/dist/routes/pages.js.map +1 -0
  265. package/dist/routes/panel.d.ts +19 -0
  266. package/dist/routes/panel.d.ts.map +1 -0
  267. package/dist/routes/panel.js +191 -0
  268. package/dist/routes/panel.js.map +1 -0
  269. package/dist/routes/relations.d.ts +21 -0
  270. package/dist/routes/relations.d.ts.map +1 -0
  271. package/dist/routes/relations.js +1239 -0
  272. package/dist/routes/relations.js.map +1 -0
  273. package/dist/routes/resources.d.ts +28 -0
  274. package/dist/routes/resources.d.ts.map +1 -0
  275. package/dist/routes/resources.js +741 -0
  276. package/dist/routes/resources.js.map +1 -0
  277. package/dist/routes/theme.d.ts +12 -0
  278. package/dist/routes/theme.d.ts.map +1 -0
  279. package/dist/routes/theme.js +82 -0
  280. package/dist/routes/theme.js.map +1 -0
  281. package/dist/routes.d.ts.map +1 -1
  282. package/dist/routes.js +64 -3078
  283. package/dist/routes.js.map +1 -1
  284. package/dist/vite.d.ts +1 -0
  285. package/dist/vite.d.ts.map +1 -1
  286. package/dist/vite.js +31 -10
  287. package/dist/vite.js.map +1 -1
  288. package/package.json +2 -1
  289. package/src/Pilotiq.ts +95 -0
  290. package/src/actions/Action.ts +79 -723
  291. package/src/actions/bulkFactories.ts +168 -0
  292. package/src/actions/crudFactories.ts +220 -0
  293. package/src/actions/factoryHelpers.ts +177 -0
  294. package/src/actions/m2mFactories.ts +193 -0
  295. package/src/actions/relationFactories.ts +372 -0
  296. package/src/elements/dispatchForm.ts +1 -1
  297. package/src/elements/dispatchTable.ts +1 -1
  298. package/src/fields/Field.ts +39 -0
  299. package/src/pageData/breadcrumbs.ts +288 -0
  300. package/src/pageData/forms.ts +578 -0
  301. package/src/pageData/helpers.ts +764 -0
  302. package/src/pageData/misc.ts +347 -0
  303. package/src/pageData/navigation.ts +779 -0
  304. package/src/pageData/relationPages.ts +1246 -0
  305. package/src/pageData/relationTabs.ts +286 -0
  306. package/src/pageData/resourcePages.ts +593 -0
  307. package/src/pageData.ts +122 -4731
  308. package/src/react/AppShell.tsx +27 -1
  309. package/src/react/CollabExtensionFactoryRegistry.ts +55 -0
  310. package/src/react/CollabRoomContext.ts +42 -0
  311. package/src/react/FormCollabBindingRegistry.ts +72 -0
  312. package/src/react/RecordWrapperGate.tsx +40 -0
  313. package/src/react/RecordWrapperRegistry.ts +39 -0
  314. package/src/react/SchemaRenderer.tsx +230 -6479
  315. package/src/react/component-slots.test.ts +103 -0
  316. package/src/react/component-slots.ts +116 -0
  317. package/src/react/fields/BuilderInput.tsx +29 -117
  318. package/src/react/fields/MarkdownInput.tsx +0 -1
  319. package/src/react/fields/RepeaterInput.tsx +29 -130
  320. package/src/react/fields/rowState.ts +106 -0
  321. package/src/react/fields/useRowReorderDnd.ts +78 -0
  322. package/src/react/index.ts +38 -0
  323. package/src/react/layouts/SidebarLayout.tsx +39 -28
  324. package/src/react/layouts/TopbarLayout.tsx +70 -57
  325. package/src/react/parseRecordEditUrl.test.ts +75 -0
  326. package/src/react/parseRecordEditUrl.ts +55 -0
  327. package/src/react/persistedState.ts +40 -0
  328. package/src/react/schemaRenderer/AlertRenderer.tsx +112 -0
  329. package/src/react/schemaRenderer/EntryRenderer.tsx +501 -0
  330. package/src/react/schemaRenderer/SectionRenderer.tsx +120 -0
  331. package/src/react/schemaRenderer/SimpleElements.tsx +306 -0
  332. package/src/react/schemaRenderer/TabsRenderer.tsx +62 -0
  333. package/src/react/schemaRenderer/WizardRenderer.tsx +338 -0
  334. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +177 -0
  335. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +273 -0
  336. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +61 -0
  337. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +43 -0
  338. package/src/react/schemaRenderer/action/MethodActionButton.tsx +64 -0
  339. package/src/react/schemaRenderer/action/buttons.tsx +99 -0
  340. package/src/react/schemaRenderer/action/helpers.ts +140 -0
  341. package/src/react/schemaRenderer/action/renderAction.tsx +245 -0
  342. package/src/react/schemaRenderer/columnFormat.ts +65 -0
  343. package/src/react/schemaRenderer/constants.ts +50 -0
  344. package/src/react/schemaRenderer/form/FormRenderer.tsx +233 -0
  345. package/src/react/schemaRenderer/form/renderField.tsx +511 -0
  346. package/src/react/schemaRenderer/helpers.tsx +81 -0
  347. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +308 -0
  348. package/src/react/schemaRenderer/table/TableRenderer.tsx +123 -0
  349. package/src/react/schemaRenderer/table/TableRendererBody.tsx +974 -0
  350. package/src/react/schemaRenderer/table/filters.tsx +1233 -0
  351. package/src/react/schemaRenderer/table/formatCell.tsx +264 -0
  352. package/src/react/schemaRenderer/table/links.tsx +112 -0
  353. package/src/react/schemaRenderer/table/renderRowActions.tsx +52 -0
  354. package/src/react/schemaRenderer/table/url.tsx +143 -0
  355. package/src/routes/globals.ts +154 -0
  356. package/src/routes/helpers.ts +668 -0
  357. package/src/routes/pages.ts +173 -0
  358. package/src/routes/panel.ts +204 -0
  359. package/src/routes/relations.ts +1219 -0
  360. package/src/routes/resources.ts +786 -0
  361. package/src/routes/theme.ts +109 -0
  362. package/src/routes.test.ts +1 -1
  363. package/src/routes.ts +64 -3176
  364. package/src/schema/TableWidget.test.ts +2 -2
  365. package/src/theme/migrate.test.ts +178 -0
  366. package/src/vite.test.ts +184 -0
  367. package/src/vite.ts +31 -9
@@ -0,0 +1,668 @@
1
+ import type { AppRequest, AppResponse } from '@rudderjs/contracts'
2
+ import type { Pilotiq } from '../Pilotiq.js'
3
+ import { findActions, findRowExtraActions, type DispatchActionResult } from '../elements/dispatchAction.js'
4
+ import { flashNotifications } from '../notifications/flash.js'
5
+ import type { NotificationMeta } from '../notifications/Notification.js'
6
+ import { findRecord, type ModelQuery } from '../orm/modelDefaults.js'
7
+ import type { ResourceClass } from '../Resource.js'
8
+ import {
9
+ formStateData, type FormStateScope,
10
+ formWizardData,
11
+ formCreateOptionData,
12
+ mentionResolveData,
13
+ widgetData, type WidgetScope,
14
+ } from '../pageData.js'
15
+
16
+ /** True when the client wants a JSON response (modal-form action submitting
17
+ * via fetch), false for a browser-style form post that wants a 303 redirect.
18
+ * Both action endpoints honor this so confirm/handler buttons (form-post)
19
+ * keep working unchanged while modal dialogs use fetch. */
20
+ export function wantsJson(req: AppRequest): boolean {
21
+ const headers = req.headers ?? {}
22
+ const accept = headers['accept'] ?? headers['Accept'] ?? ''
23
+ return accept.includes('application/json')
24
+ }
25
+
26
+ /**
27
+ * Read the request body as a `Record<string, unknown>`. The hono adapter
28
+ * auto-parses JSON, but `application/x-www-form-urlencoded` and
29
+ * `multipart/form-data` need a manual fall-through to Hono's own parser.
30
+ */
31
+ export async function readFormBody(req: AppRequest): Promise<Record<string, unknown>> {
32
+ if (req.body && typeof req.body === 'object' && !Array.isArray(req.body)) {
33
+ return { ...(req.body as Record<string, unknown>) }
34
+ }
35
+ const raw = req.raw as { req?: { parseBody?: () => Promise<Record<string, unknown>> } } | undefined
36
+ if (raw?.req?.parseBody) {
37
+ try {
38
+ const parsed = await raw.req.parseBody()
39
+ return parsed && typeof parsed === 'object' ? { ...parsed } : {}
40
+ } catch {
41
+ return {}
42
+ }
43
+ }
44
+ return {}
45
+ }
46
+
47
+ /**
48
+ * Normalize a user-supplied redirect URL. Returns absolute URLs and
49
+ * scheme-prefixed URLs unchanged. Bare relative paths (no leading `/`)
50
+ * are joined under the panel's `basePath` — without this, the browser
51
+ * resolves the redirect against the current request URL and produces
52
+ * paths like `/admin/articles/{id}/articles/{id}/edit`.
53
+ *
54
+ * `getRedirectUrl` page hooks and `Form.redirectAfterSave` callbacks
55
+ * are user-authored; this protects the framework against the common
56
+ * authoring slip while keeping absolute URLs (the documented form)
57
+ * working as-is.
58
+ */
59
+ export function normalizeRedirect(url: string | undefined, basePath: string): string | undefined {
60
+ if (!url) return undefined
61
+ if (url.startsWith('/')) return url
62
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return url // http(s):, mailto:, etc.
63
+ const trimmedBase = basePath.replace(/\/$/, '')
64
+ return `${trimmedBase}/${url}`
65
+ }
66
+
67
+ /** Strip framework meta keys (`_formId`, `_method`, `_continueCreate`)
68
+ * from a parsed body. `continueCreate` mirrors the secondary
69
+ * "Create & create another" submit on `CreatePage`: when `'1'`, the
70
+ * create POST handler routes the redirect back to the create URL
71
+ * instead of the new record's edit page. */
72
+ export function splitMeta(body: Record<string, unknown>): {
73
+ values: Record<string, unknown>
74
+ formId: string | undefined
75
+ continueCreate: boolean
76
+ } {
77
+ const { _formId, _method: _omitMethod, _continueCreate, ...rest } = body
78
+ return {
79
+ values: rest,
80
+ formId: typeof _formId === 'string' ? _formId : undefined,
81
+ continueCreate: _continueCreate === '1' || _continueCreate === 1 || _continueCreate === true,
82
+ }
83
+ }
84
+
85
+ /** Strip control characters (`"\\\r\n`) from a download filename so
86
+ * the `Content-Disposition: attachment; filename="…"` header stays
87
+ * unbreakable. Defends against a handler that returns a hostile
88
+ * filename string. Empty fallback `'export'`. */
89
+ export function sanitizeFilename(name: string): string {
90
+ const cleaned = (name ?? '').replace(/[\r\n"\\]/g, '').trim()
91
+ return cleaned.length > 0 ? cleaned : 'export'
92
+ }
93
+
94
+ /** Write an `Action`-handler download envelope as the response. Sets
95
+ * `Content-Type` + `Content-Disposition: attachment` and ends with
96
+ * the body. Mutually exclusive with redirect — call sites consult
97
+ * `result.download` first. */
98
+ export function sendDownload(
99
+ res: AppResponse,
100
+ env: { filename: string; contentType: string; body: string },
101
+ ): void {
102
+ res.header('Content-Type', env.contentType)
103
+ res.header('Content-Disposition', `attachment; filename="${sanitizeFilename(env.filename)}"`)
104
+ res.send(env.body)
105
+ }
106
+
107
+ /** Low-level success-or-redirect responder. Either emits a JSON envelope
108
+ * (`{ ok: true, redirect, notifications?, force? }`) when the client
109
+ * asked for JSON, or flashes notifications to the next request and
110
+ * issues a 303 redirect. Shared by `sendActionResult`,
111
+ * `sendMutationSuccess`, and the form-submit success branches in
112
+ * resources / globals / pages / relations. */
113
+ export function sendRedirectResponse(
114
+ req: AppRequest,
115
+ res: AppResponse,
116
+ json: boolean,
117
+ redirect: string,
118
+ notifications: ReadonlyArray<NotificationMeta> | undefined,
119
+ extras?: { force?: boolean },
120
+ ): unknown {
121
+ if (json) {
122
+ return res.json({
123
+ ok: true,
124
+ redirect,
125
+ ...(extras?.force ? { force: true } : {}),
126
+ ...(notifications && notifications.length > 0 ? { notifications } : {}),
127
+ })
128
+ }
129
+ flashNotifications(req, notifications)
130
+ return res.redirect(redirect, 303)
131
+ }
132
+
133
+ /** Action-dispatch result responder. Branches on `result.ok` / download
134
+ * envelope / redirect. Consumed by every `_action/:actionName` endpoint
135
+ * in resources / pages / relations.
136
+ *
137
+ * - `!result.ok` → 422 (with `errors`) or 500 (`error` only). JSON path
138
+ * returns `{ ok: false, error, errors? }`; HTML path sends `error`.
139
+ * - `result.download` → calls `sendDownload` (mutually exclusive with
140
+ * `redirect`; binary response has no JSON envelope to carry
141
+ * notifications).
142
+ * - otherwise → `sendRedirectResponse` with the normalized redirect
143
+ * (defaulting to `fallbackUrl`) and `result.notifications`. */
144
+ export function sendActionResult(
145
+ req: AppRequest,
146
+ res: AppResponse,
147
+ json: boolean,
148
+ result: DispatchActionResult,
149
+ base: string,
150
+ fallbackUrl: string,
151
+ ): unknown {
152
+ if (!result.ok) {
153
+ if (json) {
154
+ res.status(result.errors ? 422 : 500)
155
+ return res.json({
156
+ ok: false,
157
+ error: result.error,
158
+ ...(result.errors ? { errors: result.errors } : {}),
159
+ })
160
+ }
161
+ res.status(500)
162
+ return res.send(result.error)
163
+ }
164
+ if (result.download) return sendDownload(res, result.download)
165
+ const redirect = normalizeRedirect(result.redirect, base) ?? fallbackUrl
166
+ return sendRedirectResponse(req, res, json, redirect, result.notifications)
167
+ }
168
+
169
+ /** Synthetic-notification mutation responder for delete / restore /
170
+ * force-delete / detach routes. The form-method 303 path doesn't have
171
+ * the form-lifecycle toast pipeline; this synthesizes one
172
+ * notification so the SPA path gets the same toast UX as a JSON-
173
+ * dispatched action handler. `kind` becomes part of the notification
174
+ * id (`n-${kind}-${id}-${Date.now()}`) so the client can dedupe. */
175
+ export function sendMutationSuccess(
176
+ req: AppRequest,
177
+ res: AppResponse,
178
+ json: boolean,
179
+ opts: { id: string; kind: string; title: string; redirect: string },
180
+ ): unknown {
181
+ const notifications: NotificationMeta[] = [
182
+ { id: `n-${opts.kind}-${opts.id}-${Date.now()}`, type: 'success', title: opts.title } as never,
183
+ ]
184
+ return sendRedirectResponse(req, res, json, opts.redirect, notifications)
185
+ }
186
+
187
+ /** Soft-delete record lookup. Runs `q.withTrashed().where(pk, '=', id)
188
+ * .paginate(1, 1)` and returns the first row, or `undefined` when the
189
+ * query has no `withTrashed()` method (caller falls back to a normal
190
+ * `M.find(id)` or returns 404). `.catch(...)` swallows the paginate
191
+ * error so a bad query shape surfaces as "not found" rather than a
192
+ * 500. */
193
+ export async function findInQueryWithTrashed(
194
+ q: ModelQuery,
195
+ pk: string,
196
+ id: string,
197
+ ): Promise<unknown> {
198
+ if (typeof q.withTrashed !== 'function') return undefined
199
+ const result = await q.withTrashed()
200
+ .where(pk, '=', id)
201
+ .paginate(1, 1)
202
+ .catch(() => ({ data: [] as unknown[] }))
203
+ return Array.isArray(result.data) ? result.data[0] : undefined
204
+ }
205
+
206
+ /** Plan #10 — send a 403 response. Branches on `Accept: application/json`
207
+ * the same way the action / form dispatch paths do. Used by every route
208
+ * after a `Resource.canX(...)` check fails. We deliberately do NOT
209
+ * redirect to login: 403 means "authenticated but not allowed"; the
210
+ * 401-unauthenticated case is `Pilotiq.guard()`'s job. */
211
+ export function forbidden(res: AppResponse, json: boolean): unknown {
212
+ res.status(403)
213
+ if (json) return res.json({ ok: false, error: 'Forbidden' })
214
+ return res.send('Forbidden')
215
+ }
216
+
217
+ /** Extract a user-facing message from a thrown value inside an editable
218
+ * column's beforeStateUpdated / afterStateUpdated hook. Stamped under
219
+ * the reserved `_cell` key in the 422 response. */
220
+ export function cellHookErrorMessage(err: unknown): string {
221
+ if (err instanceof Error && err.message) return err.message
222
+ if (typeof err === 'string' && err.length > 0) return err
223
+ return 'Update halted'
224
+ }
225
+
226
+ /** Run a `canX(...)` predicate, treating throws as `false`. The predicate
227
+ * is user-authored and we want a flaky check to fail closed (deny) rather
228
+ * than 500 the page. */
229
+ export async function checkPolicy(fn: () => boolean | Promise<boolean>): Promise<boolean> {
230
+ try { return Boolean(await fn()) } catch { return false }
231
+ }
232
+
233
+ export async function policyAccess(
234
+ owner: {
235
+ canAccess: (user: unknown) => boolean | Promise<boolean>
236
+ cluster?: { canAccess: (user: unknown) => boolean | Promise<boolean> }
237
+ },
238
+ user: unknown,
239
+ ): Promise<boolean> {
240
+ const [ownerOk, clusterOk] = await Promise.all([
241
+ checkPolicy(() => owner.canAccess(user)),
242
+ owner.cluster
243
+ ? checkPolicy(() => owner.cluster!.canAccess(user))
244
+ : Promise.resolve(true),
245
+ ])
246
+ return ownerOk && clusterOk
247
+ }
248
+
249
+ /** Run `policyAccess(R, user)` and `findRecord(R, recordId, { user })`
250
+ * in parallel. Both depend only on `user`, so the two round-trips
251
+ * overlap instead of waiting on each other. When `R.model` isn't set
252
+ * the record falls back to a stub `{ id: recordId }` so record-aware
253
+ * predicates can still pattern-match. Throws are caught — a failed
254
+ * record load resolves to `undefined`, mirroring the pre-helper inline
255
+ * shape `findRecord(...).catch(() => undefined)`. */
256
+ export async function loadAccessGated(
257
+ R: ResourceClass,
258
+ recordId: string,
259
+ user: unknown,
260
+ ): Promise<{ access: boolean; record: unknown }> {
261
+ const [access, record] = await Promise.all([
262
+ policyAccess(R, user),
263
+ R.model
264
+ ? findRecord(R, recordId, { user }).catch(() => undefined)
265
+ : Promise.resolve({ id: recordId }),
266
+ ])
267
+ return { access, record }
268
+ }
269
+
270
+ /**
271
+ * Locate an action by name in a resolved page schema. Looks at both
272
+ * page-level actions (`findActions`) AND row-scoped extraItemActions on
273
+ * Repeater/Builder fields (`findRowExtraActions`). When the match is
274
+ * row-scoped, also returns the parent field reference and the form
275
+ * schema array — the dispatcher uses both to coerce the form body and
276
+ * navigate to the right row when stamping `ctx.row`.
277
+ *
278
+ * Page-level matches win when a page-level + row-scoped action share the
279
+ * same name (page-level is strictly more privileged: it has access to
280
+ * the full form, not just one row). The collision is undocumented
281
+ * behavior — authors should use distinct names.
282
+ */
283
+ export function resolveDispatchTarget(
284
+ elements: import('../schema/Element.js').Element[],
285
+ actionName: string,
286
+ ): {
287
+ action: import('../actions/Action.js').Action
288
+ rowField?: import('../fields/RepeaterField.js').RepeaterField | import('../fields/BuilderField.js').BuilderField
289
+ formSchema?: import('../schema/Element.js').Element[]
290
+ } | null {
291
+ const pageLevel = findActions(elements).find(a => a.name === actionName)
292
+ if (pageLevel) return { action: pageLevel }
293
+
294
+ const rowMatches = findRowExtraActions(elements).filter(r => r.action.name === actionName)
295
+ if (rowMatches.length === 0) return null
296
+ if (rowMatches.length > 1) {
297
+ console.warn(
298
+ `[pilotiq] Action "${actionName}" registered as extraItemActions on multiple ` +
299
+ `fields. Using the first match — disambiguate by renaming.`,
300
+ )
301
+ }
302
+ const first = rowMatches[0]!
303
+ // `formSchema` is the entire page tree for v1 — `coerceFormValues`
304
+ // needs the field schema rooted at the form, not just the one row's
305
+ // children. Passing the page tree is over-broad but safe (the function
306
+ // walks until it finds the field). A future polish can narrow to the
307
+ // owning Form once we walk back from the matched field.
308
+ return { action: first.action, rowField: first.field, formSchema: elements }
309
+ }
310
+
311
+ /**
312
+ * Plan #5 — handle a partial-resolve POST. The body shape is
313
+ * `{ changed, values }`; `formId` comes from the URL path. Response
314
+ * is `{ ok, form, dirty }` on success or `{ ok: false, error }` for
315
+ * missing form / unknown field.
316
+ */
317
+ export interface FormStateBody {
318
+ changed?: unknown
319
+ values?: unknown
320
+ }
321
+
322
+ export async function handleFormState(
323
+ req: AppRequest,
324
+ res: AppResponse,
325
+ pilotiq: Pilotiq,
326
+ scope: FormStateScope,
327
+ formId: string,
328
+ ): Promise<unknown> {
329
+ const body = (await readFormBody(req)) as FormStateBody
330
+ const changed = typeof body.changed === 'string' ? body.changed : ''
331
+ const values = (body.values && typeof body.values === 'object' && !Array.isArray(body.values))
332
+ ? body.values as Record<string, unknown>
333
+ : {}
334
+ if (!formId || !changed) {
335
+ res.status(400)
336
+ return res.json({ ok: false, error: 'Missing formId or changed field' })
337
+ }
338
+
339
+ try {
340
+ const result = await formStateData(pilotiq, scope, { formId, changed, values }, req)
341
+ if (result === null) {
342
+ res.status(404)
343
+ return res.json({ ok: false, error: 'Page not found' })
344
+ }
345
+ if (!result.ok) {
346
+ res.status(result.status)
347
+ return res.json({ ok: false, error: result.error })
348
+ }
349
+ return res.json({ ok: true, form: result.form, dirty: result.dirty })
350
+ } catch (err) {
351
+ const message = err instanceof Error ? err.message : 'Form update failed'
352
+ res.status(500)
353
+ return res.json({ ok: false, error: message })
354
+ }
355
+ }
356
+
357
+ export interface FormWizardBody {
358
+ step?: unknown
359
+ values?: unknown
360
+ }
361
+
362
+ export async function handleFormWizard(
363
+ req: AppRequest,
364
+ res: AppResponse,
365
+ pilotiq: Pilotiq,
366
+ scope: FormStateScope,
367
+ formId: string,
368
+ ): Promise<unknown> {
369
+ const body = (await readFormBody(req)) as FormWizardBody
370
+ const stepN = typeof body.step === 'number' ? body.step
371
+ : typeof body.step === 'string' ? Number(body.step)
372
+ : NaN
373
+ const values = (body.values && typeof body.values === 'object' && !Array.isArray(body.values))
374
+ ? body.values as Record<string, unknown>
375
+ : {}
376
+ if (!formId || !Number.isFinite(stepN) || stepN < 0) {
377
+ res.status(400)
378
+ return res.json({ ok: false, error: 'Missing formId or invalid step' })
379
+ }
380
+
381
+ try {
382
+ const result = await formWizardData(pilotiq, scope, { formId, step: stepN, values }, req)
383
+ if (result === null) {
384
+ res.status(404)
385
+ return res.json({ ok: false, error: 'Page not found' })
386
+ }
387
+ if (!result.ok) {
388
+ res.status(result.status)
389
+ const payload: Record<string, unknown> = { ok: false }
390
+ if (result.error) payload['error'] = result.error
391
+ if (result.errors) payload['errors'] = result.errors
392
+ return res.json(payload)
393
+ }
394
+ return res.json({ ok: true })
395
+ } catch (err) {
396
+ const message = err instanceof Error ? err.message : 'Wizard step validation failed'
397
+ res.status(500)
398
+ return res.json({ ok: false, error: message })
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Audit row 2026-05-07 cont'd⁸ — `SelectField.createOptionForm()` modal
404
+ * submit. Body carries `{ values }`; `formId` + `fieldName` come from
405
+ * the URL path. Returns `{ ok, option: { value, label } }` on success
406
+ * or `{ ok: false, error }` for missing scope / form / field, 403 for
407
+ * authorize failure, or 422 with `errors` for validation.
408
+ *
409
+ * One handler shared across all four scopes (resource-create /
410
+ * resource-edit / global-edit / custom-page) — caller passes the
411
+ * matching `FormStateScope` so the same `canAccess + canCreate / canEdit`
412
+ * predicates apply to the parent form's policy gate.
413
+ */
414
+ export interface FormCreateOptionBody {
415
+ values?: unknown
416
+ }
417
+
418
+ export async function handleFormCreateOption(
419
+ req: AppRequest,
420
+ res: AppResponse,
421
+ pilotiq: Pilotiq,
422
+ scope: FormStateScope,
423
+ formId: string,
424
+ fieldName: string,
425
+ ): Promise<unknown> {
426
+ const body = (await readFormBody(req)) as FormCreateOptionBody
427
+ const values = (body.values && typeof body.values === 'object' && !Array.isArray(body.values))
428
+ ? body.values as Record<string, unknown>
429
+ : {}
430
+ if (!formId || !fieldName) {
431
+ res.status(400)
432
+ return res.json({ ok: false, error: 'Missing formId or fieldName' })
433
+ }
434
+
435
+ try {
436
+ const result = await formCreateOptionData(pilotiq, scope, { formId, fieldName, values }, req)
437
+ if (result === null) {
438
+ res.status(404)
439
+ return res.json({ ok: false, error: 'Page not found' })
440
+ }
441
+ if (!result.ok) {
442
+ res.status(result.status)
443
+ const payload: Record<string, unknown> = { ok: false }
444
+ if (result.error) payload['error'] = result.error
445
+ if (result.errors) payload['errors'] = result.errors
446
+ return res.json(payload)
447
+ }
448
+ return res.json({ ok: true, option: result.option })
449
+ } catch (err) {
450
+ const message = err instanceof Error ? err.message : 'createOption failed'
451
+ res.status(500)
452
+ return res.json({ ok: false, error: message })
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Async-mention round-trip handler. Body is `{ field, trigger, query }`;
458
+ * `formId` comes from the URL path. Returns `{ ok, items }` on success
459
+ * or `{ ok: false, error }` for missing form / field / trigger.
460
+ *
461
+ * Each scope (resource-create, resource-edit, global-edit, custom-page)
462
+ * registers its own route — the auth gate matches the matching `_form/
463
+ * :formId/state` endpoint so the same `canAccess + canCreate / canEdit`
464
+ * predicates apply.
465
+ */
466
+ export interface FormMentionsBody {
467
+ field?: unknown
468
+ trigger?: unknown
469
+ query?: unknown
470
+ }
471
+
472
+ export async function handleFormMentions(
473
+ req: AppRequest,
474
+ res: AppResponse,
475
+ pilotiq: Pilotiq,
476
+ scope: FormStateScope,
477
+ formId: string,
478
+ ): Promise<unknown> {
479
+ const body = (await readFormBody(req)) as FormMentionsBody
480
+ const field = typeof body.field === 'string' ? body.field : ''
481
+ const trigger = typeof body.trigger === 'string' ? body.trigger : ''
482
+ const query = typeof body.query === 'string' ? body.query : ''
483
+ if (!formId || !field || trigger.length !== 1) {
484
+ res.status(400)
485
+ return res.json({ ok: false, error: 'Missing formId / field / trigger' })
486
+ }
487
+
488
+ // Cap query length — the resolver runs the user's code; the trigger
489
+ // never sends more than a word's worth of characters in practice.
490
+ const cappedQuery = query.length > 200 ? query.slice(0, 200) : query
491
+
492
+ try {
493
+ const result = await mentionResolveData(
494
+ pilotiq,
495
+ scope,
496
+ { formId, field, trigger, query: cappedQuery },
497
+ req,
498
+ )
499
+ if (result === null) {
500
+ res.status(404)
501
+ return res.json({ ok: false, error: 'Page not found' })
502
+ }
503
+ if (!result.ok) {
504
+ res.status(result.status)
505
+ return res.json({ ok: false, error: result.error })
506
+ }
507
+ return res.json({ ok: true, items: result.items })
508
+ } catch (err) {
509
+ const message = err instanceof Error ? err.message : 'Mention resolve failed'
510
+ res.status(500)
511
+ return res.json({ ok: false, error: message })
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Plan #15 — handle a widget polling POST. Body is `{ filter? }`;
517
+ * `:id` comes from the URL. Returns `{ ok, data, timestamp }` on
518
+ * success or `{ ok: false, error }` on failure. Used by lazy-loading
519
+ * widgets (first fetch on mount) and `poll(seconds)` widgets (interval
520
+ * re-fetch).
521
+ */
522
+ export interface WidgetBody {
523
+ filter?: unknown
524
+ }
525
+
526
+ export async function handleWidgetData(
527
+ req: AppRequest,
528
+ res: AppResponse,
529
+ pilotiq: Pilotiq,
530
+ scope: WidgetScope,
531
+ id: string,
532
+ ): Promise<unknown> {
533
+ if (!id) {
534
+ res.status(400)
535
+ return res.json({ ok: false, error: 'Missing widget id' })
536
+ }
537
+ const body = (await readFormBody(req)) as WidgetBody
538
+ const filter = typeof body.filter === 'string' ? body.filter : undefined
539
+
540
+ try {
541
+ const result = await widgetData(
542
+ pilotiq,
543
+ scope,
544
+ filter !== undefined ? { id, filter } : { id },
545
+ req,
546
+ )
547
+ if (!result.ok) {
548
+ res.status(result.status)
549
+ return res.json({ ok: false, error: result.error })
550
+ }
551
+ return res.json({ ok: true, data: result.data, timestamp: result.timestamp })
552
+ } catch (err) {
553
+ res.status(500)
554
+ return res.json({ ok: false, error: err instanceof Error ? err.message : 'Widget request failed' })
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Handle a single file upload from a `FileUpload` field. Validates
560
+ * accept / maxSize against the (optional) per-request hints, hands
561
+ * the file off to the configured adapter, returns `{ ok, url }`.
562
+ *
563
+ * Body shape (multipart/form-data):
564
+ * - `file`: the file blob
565
+ * - `directory`: optional sub-directory hint
566
+ * - `accept`: optional comma-separated MIME list to enforce
567
+ * - `maxSize`: optional byte cap
568
+ * - `fieldName`: optional tag forwarded to the adapter for routing
569
+ */
570
+ export async function handleUploadRequest(
571
+ req: AppRequest,
572
+ res: AppResponse,
573
+ pilotiq: Pilotiq,
574
+ ): Promise<unknown> {
575
+ const cfg = pilotiq.getConfig()
576
+ if (!cfg.uploads) {
577
+ res.status(500)
578
+ return res.json({ ok: false, error: 'No upload adapter configured' })
579
+ }
580
+
581
+ // Auth: panel-wide `guard` and per-request `user`. We don't enforce
582
+ // per-resource canEdit here because the field doesn't know which
583
+ // resource it belongs to — apps that need it should hook into
584
+ // their adapter's `put()` and consult their own auth there.
585
+ if (cfg.guard && !await cfg.guard(req)) {
586
+ res.status(401)
587
+ return res.json({ ok: false, error: 'Unauthorized' })
588
+ }
589
+
590
+ // Parse multipart body. Hono's parseBody returns `Record<string, File | string>`.
591
+ const raw = req.raw as { req?: { parseBody?: (opts?: { all?: boolean }) => Promise<Record<string, unknown>> } } | undefined
592
+ if (!raw?.req?.parseBody) {
593
+ res.status(500)
594
+ return res.json({ ok: false, error: 'Multipart parsing unavailable' })
595
+ }
596
+ let body: Record<string, unknown>
597
+ try {
598
+ body = await raw.req.parseBody()
599
+ } catch (err) {
600
+ res.status(400)
601
+ return res.json({ ok: false, error: err instanceof Error ? err.message : 'Bad request' })
602
+ }
603
+
604
+ const file = body['file']
605
+ if (!file || !(file instanceof File)) {
606
+ res.status(422)
607
+ return res.json({ ok: false, error: 'No file provided' })
608
+ }
609
+
610
+ const directory = typeof body['directory'] === 'string' ? body['directory'] : undefined
611
+ const fieldName = typeof body['fieldName'] === 'string' ? body['fieldName'] : ''
612
+
613
+ // Server-side validation. Both accept and maxSize are advisory hints
614
+ // shipped by the field meta, so we re-check here so a tampered client
615
+ // can't bypass the limits.
616
+ const acceptStr = typeof body['accept'] === 'string' ? body['accept'] : ''
617
+ if (acceptStr) {
618
+ const accept = acceptStr.split(',').map(s => s.trim()).filter(Boolean)
619
+ if (accept.length > 0 && !accept.includes(file.type)) {
620
+ res.status(422)
621
+ return res.json({ ok: false, error: `File type "${file.type}" not allowed` })
622
+ }
623
+ }
624
+ const maxSizeStr = typeof body['maxSize'] === 'string' ? body['maxSize'] : ''
625
+ if (maxSizeStr) {
626
+ const maxSize = Number(maxSizeStr)
627
+ if (Number.isFinite(maxSize) && file.size > maxSize) {
628
+ res.status(422)
629
+ return res.json({ ok: false, error: `File exceeds ${maxSize} bytes` })
630
+ }
631
+ }
632
+
633
+ // Server-side resize via @rudderjs/image (optional peer dep). Variable-
634
+ // string `import(name)` keeps Vite's static import-analysis from trying
635
+ // to pre-resolve the module on host apps that don't have @rudderjs/image
636
+ // installed — same pattern as `notifications/database.ts` for `@rudderjs/orm`.
637
+ const resizeWidthStr = typeof body['resize_width'] === 'string' ? body['resize_width'] : ''
638
+ const resizeHeightStr = typeof body['resize_height'] === 'string' ? body['resize_height'] : ''
639
+ let uploadFile: File = file
640
+ if (resizeWidthStr && resizeHeightStr) {
641
+ const w = Number(resizeWidthStr)
642
+ const h = Number(resizeHeightStr)
643
+ if (Number.isFinite(w) && w > 0 && Number.isFinite(h) && h > 0) {
644
+ try {
645
+ const imageModuleName = '@rudderjs/image'
646
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
647
+ const pkg = await import(/* @vite-ignore */ imageModuleName) as { image: (input: unknown) => { resize(w: number, h: number): { format(f: string): { toBuffer(): Promise<Buffer> } } } }
648
+ const buf = await pkg.image(file).resize(w, h).format('webp').toBuffer()
649
+ const baseName = file.name.replace(/\.[^.]+$/, '')
650
+ uploadFile = new File([buf.buffer as ArrayBuffer], `${baseName}.webp`, { type: 'image/webp' })
651
+ } catch {
652
+ // @rudderjs/image not installed or resize failed — fall through with original file
653
+ }
654
+ }
655
+ }
656
+
657
+ try {
658
+ const result = await cfg.uploads.adapter.put({
659
+ file: uploadFile,
660
+ ...(directory ? { directory } : {}),
661
+ fieldName,
662
+ })
663
+ return res.json({ ok: true, url: result.url, ...(result.meta ? { meta: result.meta } : {}) })
664
+ } catch (err) {
665
+ res.status(500)
666
+ return res.json({ ok: false, error: err instanceof Error ? err.message : 'Upload failed' })
667
+ }
668
+ }