@pilotiq/pilotiq 0.7.2 → 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 +142 -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 +26 -5
  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 +26 -4
@@ -0,0 +1,593 @@
1
+ import type { Pilotiq } from '../Pilotiq.js'
2
+ import { PilotiqRegistry } from '../PilotiqRegistry.js'
3
+ import type { Page } from '../Page.js'
4
+ import type { ResourceClass } from '../Resource.js'
5
+ import { resourceBasePath } from '../clusterPaths.js'
6
+ import { Element } from '../schema/Element.js'
7
+ import { resolveSchema, type SchemaContext } from '../schema/resolveSchema.js'
8
+ import { Form } from '../elements/Form.js'
9
+ import { Table } from '../elements/Table.js'
10
+ import { Column } from '../Column.js'
11
+ import { ListTabs } from '../elements/ListTabs.js'
12
+ import { ListTab } from '../Tab.js'
13
+ import { TrashedFilter } from '../filters/TrashedFilter.js'
14
+ import { loadTableRecords } from '../elements/dispatchTable.js'
15
+ import { consumeFlashedNotifications } from '../notifications/flash.js'
16
+ import { serializeIcon } from '../icons/types.js'
17
+ import {
18
+ findRecord, getPrimaryKey, modelLoadRecord, modelSave,
19
+ type ModelLike,
20
+ } from '../orm/modelDefaults.js'
21
+ import {
22
+ resourceCreateBreadcrumbs,
23
+ resourceEditBreadcrumbs,
24
+ resourceListBreadcrumbs,
25
+ resourceViewBreadcrumbs,
26
+ } from './breadcrumbs.js'
27
+ import {
28
+ applyFillPipeline,
29
+ applyRelationshipBuilderFill,
30
+ applyRelationshipRepeaterFill,
31
+ callPageSchema,
32
+ resolveServerDataElements,
33
+ tagActionDispatch,
34
+ tagCellEditUrls,
35
+ tagFieldAiUrls,
36
+ tagFormActions,
37
+ tagFormSubresourceUrls,
38
+ tagTableDeferred,
39
+ tagTableReorderUrls,
40
+ tagWidgetUrls,
41
+ uploadCtx,
42
+ userCtx,
43
+ } from './helpers.js'
44
+ import { applyRoleHooks, panelInfo, type PanelInfoRoute } from './navigation.js'
45
+ import { findForms } from '../elements/dispatchForm.js'
46
+ import { buildRelationTabs, deriveParentTitle, safeBool } from './relationTabs.js'
47
+
48
+ // ─── Resource page builders ─────────────────────────────────
49
+ //
50
+ // Resource-scoped GET-route data builders: dashboard, list, table
51
+ // (deferred-load shell), create, edit, view, and the optional record
52
+ // sub-page roles. Each loads its own context (record / parent / etc.),
53
+ // resolves the page schema through `resolveSchema`, stamps URL-tag
54
+ // helpers for dispatch endpoints, and returns the wire-shape envelope
55
+ // the page renderer consumes.
56
+
57
+
58
+ /**
59
+ * Per-row stamping spine shared by SSR `resourceIndexData` and the
60
+ * deferred-load JSON endpoint `resourceTableData`. Both walk the same
61
+ * Table tree and need the same dispatch / active-tab / reorder / cell-edit
62
+ * URL stamps in the same order — running them through one helper keeps
63
+ * the two paths in lock-step so any future addition (per-row chrome
64
+ * stamp, etc.) lands on both surfaces.
65
+ *
66
+ * Widget URL stamping + the `tagTableDeferred` flag stay outside the
67
+ * helper since only `resourceIndexData` mounts widgets / the deferred
68
+ * skeleton — the JSON endpoint re-runs without the flag and never
69
+ * collects widget metas.
70
+ */
71
+ async function prepareResourceTable(
72
+ elements: Element[],
73
+ R: ResourceClass,
74
+ indexUrl: string,
75
+ query: Record<string, string>,
76
+ user: unknown,
77
+ ): Promise<void> {
78
+ tagActionDispatch(elements, indexUrl)
79
+ // Mark the active tab + parallel-eval badges + stamp per-tab URLs
80
+ // before the table records run — `loadTableRecords` walks the schema
81
+ // for the active tab and splices its `modifyQuery` predicate into the
82
+ // ORM chain alongside filters.
83
+ await resolveActiveTab(elements, query, indexUrl)
84
+ await loadTableRecords(elements, query, indexUrl, user, {
85
+ canEdit: (u, record) => R.canEdit(u, record),
86
+ })
87
+ tagTableReorderUrls(elements, `${indexUrl}/_reorder`)
88
+ tagCellEditUrls(elements, indexUrl)
89
+ }
90
+
91
+ export async function dashboardData(pilotiq: Pilotiq, req?: unknown): Promise<Record<string, unknown>> {
92
+ const cfg = pilotiq.getConfig()
93
+ const user = await pilotiq.resolveUser(req)
94
+ const ctx: SchemaContext = uploadCtx(userCtx({ basePath: cfg.path }, user), cfg)
95
+
96
+ // Plan #15 — when `panel.dashboard(P)` was called, resolve P's
97
+ // schema instead of the builder-level `cfg.schema`. Page-scoped
98
+ // schema means widget elements read like a regular custom page —
99
+ // including action dispatch, form-state, and `_widget/:id` polling.
100
+ let elements: Element[]
101
+ if (cfg.dashboardPage) {
102
+ elements = await callPageSchema(cfg.dashboardPage, ctx)
103
+ tagFormActions(elements, cfg.path)
104
+ tagFormSubresourceUrls(elements, cfg.path)
105
+ tagActionDispatch(elements, cfg.path)
106
+ } else {
107
+ elements = []
108
+ if (cfg.schema) {
109
+ const def = cfg.schema
110
+ elements = typeof def === 'function' ? await def(ctx) : def
111
+ }
112
+ }
113
+
114
+ // Stamp polling URLs on every widget — panel-scope (no pageSlug
115
+ // segment) for the dashboard. Done before schema resolve so the URL
116
+ // rides on each widget's stamped meta.
117
+ tagWidgetUrls(elements, id => `${cfg.path}/_widget/${id}`)
118
+
119
+ const widgetData = await resolveServerDataElements(elements, ctx)
120
+ const dashRoute: PanelInfoRoute = cfg.dashboardPage ? { page: cfg.dashboardPage } : {}
121
+ const [panel, schemaData] = await Promise.all([
122
+ panelInfo(pilotiq, req, dashRoute),
123
+ resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'dashboard', metas, dashRoute)),
124
+ ])
125
+
126
+ return {
127
+ panel,
128
+ page: cfg.dashboardPage ? cfg.dashboardPage.toMeta() : undefined,
129
+ basePath: cfg.path,
130
+ layout: cfg.layout,
131
+ schemaData,
132
+ _widgetData: widgetData,
133
+ notifications: consumeFlashedNotifications(req),
134
+ }
135
+ }
136
+
137
+ export async function resourceIndexData(
138
+ pilotiq: Pilotiq,
139
+ slug: string,
140
+ query: Record<string, string> = {},
141
+ req?: unknown,
142
+ ): Promise<Record<string, unknown> | null> {
143
+ const cfg = pilotiq.getConfig()
144
+ const R = cfg.resources.find(r => r.getSlug() === slug)
145
+ if (!R) return null
146
+
147
+ const pages = R.resolvePages()
148
+ if (!pages.index) return null
149
+ const PageClass = pages.index
150
+
151
+ const indexUrl = resourceBasePath(cfg.path, R)
152
+ const user = await pilotiq.resolveUser(req)
153
+ const ctx: SchemaContext = uploadCtx(userCtx({ mode: 'table', basePath: cfg.path }, user), cfg)
154
+ const elements = await callPageSchema(PageClass, ctx)
155
+ // Plan #15 — resource-scope widget polling URL. Stamped before the
156
+ // schema resolves so each widget's meta carries its endpoint.
157
+ tagWidgetUrls(elements, id => `${indexUrl}/_widget/${id}`)
158
+ if (R.deferLoading) tagTableDeferred(elements, `${indexUrl}/_table`)
159
+ await prepareResourceTable(elements, R, indexUrl, query, user)
160
+ const widgetData = await resolveServerDataElements(elements, ctx)
161
+
162
+ const breadcrumbs = resourceListBreadcrumbs(cfg, R)
163
+ if (breadcrumbs) elements.unshift(breadcrumbs)
164
+
165
+ const listRoute: PanelInfoRoute = { resource: R, page: PageClass }
166
+ const [panel, schemaData] = await Promise.all([
167
+ panelInfo(pilotiq, req, listRoute),
168
+ resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'list', metas, listRoute)),
169
+ ])
170
+
171
+ return {
172
+ pageType: 'resource',
173
+ panel,
174
+ page: PageClass.toMeta(),
175
+ resource: { name: R.name, label: R.label, labelSingular: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
176
+ basePath: cfg.path,
177
+ layout: cfg.layout,
178
+ schemaData,
179
+ _widgetData: widgetData,
180
+ notifications: consumeFlashedNotifications(req),
181
+ }
182
+ }
183
+
184
+ // Deferred-load JSON endpoint payload — `GET {base}/{slug}/_table`
185
+ // re-runs the list-page builder without the deferred flag, then returns
186
+ // every resolved `TableMeta` as a flat array. Returns null on missing
187
+ // resource / index page (route 404s).
188
+ export async function resourceTableData(
189
+ pilotiq: Pilotiq,
190
+ slug: string,
191
+ query: Record<string, string> = {},
192
+ req?: unknown,
193
+ ): Promise<{ tables: Record<string, unknown>[] } | null> {
194
+ const cfg = pilotiq.getConfig()
195
+ const R = cfg.resources.find(r => r.getSlug() === slug)
196
+ if (!R) return null
197
+
198
+ const pages = R.resolvePages()
199
+ if (!pages.index) return null
200
+ const PageClass = pages.index
201
+
202
+ const indexUrl = resourceBasePath(cfg.path, R)
203
+ const user = await pilotiq.resolveUser(req)
204
+ const ctx: SchemaContext = uploadCtx(userCtx({ mode: 'table', basePath: cfg.path }, user), cfg)
205
+ const elements = await callPageSchema(PageClass, ctx)
206
+ await prepareResourceTable(elements, R, indexUrl, query, user)
207
+ const schemaData = await resolveSchema(elements, ctx)
208
+
209
+ const tables = collectTableMetas(schemaData)
210
+ return { tables }
211
+ }
212
+
213
+ function collectTableMetas(
214
+ metas: ReadonlyArray<Record<string, unknown>>,
215
+ ): Record<string, unknown>[] {
216
+ const out: Record<string, unknown>[] = []
217
+ const walk = (nodes: ReadonlyArray<Record<string, unknown>>): void => {
218
+ for (const node of nodes) {
219
+ if (node['type'] === 'table') out.push(node)
220
+ const children = node['children']
221
+ if (Array.isArray(children)) walk(children as Record<string, unknown>[])
222
+ }
223
+ }
224
+ walk(metas)
225
+ return out
226
+ }
227
+
228
+ /**
229
+ * Walk the schema for `ListTabs` containers, pick the active tab from
230
+ * `?tab=…` (defaulting to the tab marked `.default()` or the first one),
231
+ * stamp render-time state (`active` flag, per-tab `?tab=` URL, and
232
+ * resolved badge counts) onto each tab. The active tab's query/context
233
+ * modifier is NOT applied here — `loadTableRecords` walks for the active
234
+ * tab and splices in its modifier when it builds the records-handler
235
+ * `TableContext`.
236
+ *
237
+ * No-op when the page has no `ListTabs`.
238
+ */
239
+ export async function resolveActiveTab(
240
+ elements: ReadonlyArray<Element>,
241
+ query: Record<string, string>,
242
+ currentPath: string,
243
+ ): Promise<void> {
244
+ const listTabs = findListTabs(elements)
245
+ if (listTabs.length === 0) return
246
+
247
+ for (const container of listTabs) {
248
+ const children = (container.getChildren() ?? []).filter((c): c is ListTab => c.getType() === 'listTab')
249
+ if (children.length === 0) continue
250
+
251
+ // Default tab (used both for `?tab=` fallback and to omit the param
252
+ // from the canonical URL of that tab — see `buildTabUrl`).
253
+ const defaultTab = children.find(t => t.isDefault()) ?? children[0]!
254
+
255
+ // Active tab: explicit `?tab=name` → default tab.
256
+ const wanted = typeof query['tab'] === 'string' ? query['tab'] : undefined
257
+ const active = (wanted && children.find(t => t.name === wanted)) || defaultTab
258
+
259
+ // Stamp render-time state on each tab.
260
+ children.forEach(t => {
261
+ t.withActive(t === active)
262
+ t.withUrl(buildTabUrl(currentPath, query, t.name, defaultTab.name))
263
+ })
264
+
265
+ // Resolve every tab's badge in parallel — failed handlers swallow
266
+ // silently (badge omitted) so a flaky count never blanks the page.
267
+ await Promise.all(children.map(async (tab) => {
268
+ const handler = tab.getBadgeHandler()
269
+ if (!handler) return
270
+ try {
271
+ const v = await handler()
272
+ if (v === undefined || v === null) return
273
+ tab.withResolvedBadge(String(v))
274
+ } catch {
275
+ // Per-tab badge errors stay silent.
276
+ }
277
+ }))
278
+ }
279
+ }
280
+
281
+ function findListTabs(elements: ReadonlyArray<Element>): ListTabs[] {
282
+ const out: ListTabs[] = []
283
+ const walk = (els: ReadonlyArray<Element>): void => {
284
+ for (const el of els) {
285
+ if (el.getType() === 'listTabs') out.push(el as ListTabs)
286
+ const children = el.getChildren()
287
+ if (children) walk(children)
288
+ }
289
+ }
290
+ walk(elements)
291
+ return out
292
+ }
293
+
294
+ function buildTabUrl(
295
+ pathname: string,
296
+ query: Record<string, string>,
297
+ tabName: string,
298
+ defaultTabName: string,
299
+ ): string {
300
+ // Carry forward search/sort/perPage + any filter values; reset page to 1
301
+ // (tab change reshapes the result set, page numbers don't translate).
302
+ // The default tab gets the canonical, paramless URL — visiting that URL
303
+ // already lands on the default, so emitting `?tab=default` would just be
304
+ // noise that bookmarks/share-links pick up.
305
+ const params = new URLSearchParams()
306
+ for (const [k, v] of Object.entries(query)) {
307
+ if (v === undefined || v === '' || v === null) continue
308
+ if (k === 'tab' || k === 'page') continue
309
+ params.set(k, String(v))
310
+ }
311
+ if (tabName !== defaultTabName) params.set('tab', tabName)
312
+ const qs = params.toString()
313
+ return qs ? `${pathname}?${qs}` : pathname
314
+ }
315
+
316
+ export async function resourceCreateData(
317
+ pilotiq: Pilotiq,
318
+ slug: string,
319
+ prefill?: { values?: Record<string, unknown>; errors?: Record<string, string[]> },
320
+ req?: unknown,
321
+ ): Promise<Record<string, unknown> | null> {
322
+ const cfg = pilotiq.getConfig()
323
+ const R = cfg.resources.find(r => r.getSlug() === slug)
324
+ if (!R) return null
325
+ const pages = R.resolvePages()
326
+ if (!pages.create) return null
327
+ const PageClass = pages.create
328
+
329
+ const resourceBase = resourceBasePath(cfg.path, R)
330
+ const createUrl = `${resourceBase}/create`
331
+ const user = await pilotiq.resolveUser(req)
332
+ const ctx: SchemaContext = uploadCtx(userCtx({ mode: 'create', basePath: cfg.path }, user), cfg)
333
+ const elements = await callPageSchema(PageClass, ctx)
334
+ tagFormActions(elements, createUrl)
335
+ tagActionDispatch(elements, createUrl)
336
+ tagFormSubresourceUrls(elements, resourceBase)
337
+ if (prefill) {
338
+ const form = findForms(elements)[0]
339
+ if (form) {
340
+ if (prefill.values) form.withValues(prefill.values)
341
+ if (prefill.errors) form.withErrors(prefill.errors)
342
+ }
343
+ }
344
+
345
+ const breadcrumbs = resourceCreateBreadcrumbs(cfg, R)
346
+ if (breadcrumbs) elements.unshift(breadcrumbs)
347
+
348
+ const createRoute: PanelInfoRoute = { resource: R, page: PageClass }
349
+ const [panel, schemaData] = await Promise.all([
350
+ panelInfo(pilotiq, req, createRoute),
351
+ resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'create', metas, createRoute)),
352
+ ])
353
+
354
+ return {
355
+ panel,
356
+ page: PageClass.toMeta(),
357
+ resource: { name: R.name, label: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
358
+ mode: 'create' as const,
359
+ basePath: cfg.path,
360
+ layout: cfg.layout,
361
+ schemaData,
362
+ notifications: consumeFlashedNotifications(req),
363
+ ...(prefill?.errors ? { hasErrors: true } : {}),
364
+ }
365
+ }
366
+
367
+ export async function resourceEditData(
368
+ pilotiq: Pilotiq,
369
+ slug: string,
370
+ recordId: string,
371
+ prefill?: { values?: Record<string, unknown>; errors?: Record<string, string[]> },
372
+ req?: unknown,
373
+ ): Promise<Record<string, unknown> | null> {
374
+ const cfg = pilotiq.getConfig()
375
+ const R = cfg.resources.find(r => r.getSlug() === slug)
376
+ if (!R) return null
377
+ const pages = R.resolvePages()
378
+ if (!pages.edit) return null
379
+ const PageClass = pages.edit
380
+
381
+ const resourceBase = resourceBasePath(cfg.path, R)
382
+ const editUrl = `${resourceBase}/${recordId}/edit`
383
+ const user = await pilotiq.resolveUser(req)
384
+ const ctx: SchemaContext = uploadCtx(userCtx({ mode: 'edit', recordId, basePath: cfg.path }, user), cfg)
385
+ const elements = await callPageSchema(PageClass, ctx)
386
+ tagFormActions(elements, editUrl)
387
+ tagActionDispatch(elements, editUrl)
388
+ tagFormSubresourceUrls(elements, `${resourceBase}/${recordId}`)
389
+
390
+ // Locate the primary form, load the record, fill values.
391
+ const form = findForms(elements)[0]
392
+ let record: unknown = undefined
393
+ if (form?.getLoadRecord()) {
394
+ try {
395
+ record = await form.getLoadRecord()!(recordId, { values: prefill?.values ?? {} })
396
+ } catch {
397
+ // sentinel/missing record — fall through
398
+ }
399
+ if (!prefill?.values && record != null) {
400
+ const values = await applyFillPipeline(form, record)
401
+ const withRelations = await applyRelationshipRepeaterFill(form, values, record, R.model)
402
+ const withBuilders = await applyRelationshipBuilderFill(form, withRelations, record, R.model)
403
+ form.withValues(withBuilders)
404
+ } else if (prefill?.values) {
405
+ form.withValues(prefill.values)
406
+ }
407
+ if (prefill?.errors) form.withErrors(prefill.errors)
408
+ }
409
+
410
+ // Plan #11 — when the resource has relation managers, prepend a
411
+ // navigation strip so users can drill into each manager's table
412
+ // without leaving the parent record context. The "Edit" tab is
413
+ // active here.
414
+ const relationTabsEl = await buildRelationTabs(R, recordId, cfg.path, '__edit', user, record)
415
+ if (relationTabsEl) elements.unshift(relationTabsEl)
416
+
417
+ const recordTitle = record !== undefined && record !== null
418
+ ? deriveParentTitle(R, record)
419
+ : recordId
420
+ const breadcrumbs = resourceEditBreadcrumbs(cfg, R, recordId, recordTitle)
421
+ if (breadcrumbs) elements.unshift(breadcrumbs)
422
+
423
+ const editRoute: PanelInfoRoute = { resource: R, page: PageClass, recordId }
424
+ const editCtx = record !== undefined ? { ...ctx, record } : ctx
425
+ const [panel, schemaData] = await Promise.all([
426
+ panelInfo(pilotiq, req, editRoute),
427
+ resolveSchema(elements, editCtx).then(metas => applyRoleHooks(pilotiq, user, 'edit', metas, editRoute)),
428
+ ])
429
+
430
+ tagFieldAiUrls(schemaData as Record<string, unknown>[], `${resourceBase}/${recordId}/_agents`)
431
+
432
+ return {
433
+ panel,
434
+ page: PageClass.toMeta(),
435
+ resource: { name: R.name, label: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
436
+ mode: 'edit' as const,
437
+ recordId,
438
+ basePath: cfg.path,
439
+ layout: cfg.layout,
440
+ schemaData,
441
+ notifications: consumeFlashedNotifications(req),
442
+ ...(prefill?.errors ? { hasErrors: true } : {}),
443
+ }
444
+ }
445
+
446
+ export async function resourceViewData(
447
+ pilotiq: Pilotiq,
448
+ slug: string,
449
+ recordId: string,
450
+ req?: unknown,
451
+ ): Promise<Record<string, unknown> | null> {
452
+ const cfg = pilotiq.getConfig()
453
+ const R = cfg.resources.find(r => r.getSlug() === slug)
454
+ if (!R) return null
455
+ const pages = R.resolvePages()
456
+ if (!pages.view) return null
457
+ const PageClass = pages.view
458
+
459
+ const user = await pilotiq.resolveUser(req)
460
+ const ctx: SchemaContext = uploadCtx(userCtx({ mode: 'view', recordId, basePath: cfg.path }, user), cfg)
461
+ const elements = await callPageSchema(PageClass, ctx)
462
+ // For the view page we want the record threaded into resolveSchema so
463
+ // factory-attached visibility predicates see it. Resource.detail()
464
+ // already runs against the loaded record in user code; here we mirror
465
+ // that into ctx.record for the action eval pass.
466
+ let record: unknown = undefined
467
+ if (R.model) {
468
+ try { record = await findRecord(R, recordId, { user }) } catch { /* ignore */ }
469
+ }
470
+
471
+ // Plan #11 — prepend the relation tabs strip with the "Details" tab
472
+ // active when the resource has relation managers configured.
473
+ const relationTabsEl = await buildRelationTabs(R, recordId, cfg.path, '__view', user, record)
474
+ if (relationTabsEl) elements.unshift(relationTabsEl)
475
+
476
+ const recordTitle = record !== undefined && record !== null
477
+ ? deriveParentTitle(R, record)
478
+ : recordId
479
+ const breadcrumbs = resourceViewBreadcrumbs(cfg, R, recordTitle)
480
+ if (breadcrumbs) elements.unshift(breadcrumbs)
481
+
482
+ const viewRoute: PanelInfoRoute = { resource: R, page: PageClass, recordId }
483
+ const viewCtx = record !== undefined ? { ...ctx, record } : ctx
484
+ const [panel, schemaData] = await Promise.all([
485
+ panelInfo(pilotiq, req, viewRoute),
486
+ resolveSchema(elements, viewCtx).then(metas => applyRoleHooks(pilotiq, user, 'view', metas, viewRoute)),
487
+ ])
488
+
489
+ return {
490
+ panel,
491
+ page: PageClass.toMeta(),
492
+ resource: { name: R.name, label: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
493
+ mode: 'view' as const,
494
+ recordId,
495
+ basePath: cfg.path,
496
+ layout: cfg.layout,
497
+ schemaData,
498
+ notifications: consumeFlashedNotifications(req),
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Custom record sub-page data builder. Mounted at
504
+ * `${resourceBase}/${slug}/:id/${subPageSlug}` for each entry in
505
+ * `Resource.pages().record`. Mirrors `resourceViewData`'s shape: load
506
+ * the record, run R.canAccess + R.canView (parent-resource gates),
507
+ * then SubPage.canAccess(user, record) (sub-page-specific gate),
508
+ * then render the sub-page's schema with `ctx.record` set. Tab strip
509
+ * carries the sub-page slug as the active key so the matching record
510
+ * sub-page tab highlights.
511
+ *
512
+ * Returns:
513
+ * - `null` — resource / sub-page slug not found (404 upstream).
514
+ * - `{ ok: false, status: 403 }` — any gate fails or throws.
515
+ * - resolved page data — on success.
516
+ */
517
+ export async function resourceRecordPageData(
518
+ pilotiq: Pilotiq,
519
+ slug: string,
520
+ recordId: string,
521
+ subPageSlug: string,
522
+ req?: unknown,
523
+ ): Promise<Record<string, unknown> | null | { ok: false; status: 403 }> {
524
+ const cfg = pilotiq.getConfig()
525
+ const R = cfg.resources.find(r => r.getSlug() === slug)
526
+ if (!R) return null
527
+ const recordPages = R.getRecordPages()
528
+ const PageClass = recordPages[subPageSlug]
529
+ if (!PageClass) return null
530
+
531
+ const user = await pilotiq.resolveUser(req)
532
+
533
+ // Load the parent record before gating so canView / SubPage.canAccess
534
+ // can branch on record state. Sub-pages without a Resource.model
535
+ // still get gated against an `undefined` record — the same posture as
536
+ // resourceViewData when no model is bound.
537
+ let record: unknown = undefined
538
+ if (R.model) {
539
+ try { record = await findRecord(R, recordId, { user }) } catch { /* ignore */ }
540
+ }
541
+ if (record === undefined || record === null) {
542
+ // Distinguish "model bound but record missing" (route should 404)
543
+ // from "no model bound" (treat record as `{ id: recordId }` so the
544
+ // page can still render — same convention as the edit page).
545
+ if (R.model) return null
546
+ record = { id: recordId }
547
+ }
548
+
549
+ // Three gates: parent resource access + view, then the sub-page's own
550
+ // canAccess. The route would have run R.canAccess upstream, but
551
+ // re-running here makes resourceRecordPageData safe to call from
552
+ // dispatchPageData (where the SPA path skips the route prelude).
553
+ if (!await safeBool(() => R.canAccess(user))) return { ok: false, status: 403 }
554
+ if (!await safeBool(() => R.canView(user, record))) return { ok: false, status: 403 }
555
+ if (!await safeBool(() => PageClass.canAccess(user, record))) return { ok: false, status: 403 }
556
+
557
+ const ctx: SchemaContext = uploadCtx(userCtx({ mode: 'view', recordId, basePath: cfg.path }, user), cfg)
558
+ const elements = await callPageSchema(PageClass, ctx)
559
+
560
+ // Insert the relation-tabs strip with the sub-page slug active so the
561
+ // matching tab highlights. `buildRelationTabs` evaluates per-tab
562
+ // gating against `user + record` — record sub-page tabs are gated
563
+ // alongside __view/__edit/managers.
564
+ const relationTabsEl = await buildRelationTabs(R, recordId, cfg.path, subPageSlug, user, record)
565
+ if (relationTabsEl) elements.unshift(relationTabsEl)
566
+
567
+ const recordTitle = record !== undefined && record !== null
568
+ ? deriveParentTitle(R, record)
569
+ : recordId
570
+ const breadcrumbs = resourceViewBreadcrumbs(cfg, R, recordTitle)
571
+ if (breadcrumbs) elements.unshift(breadcrumbs)
572
+
573
+ const recordPageRoute: PanelInfoRoute = { resource: R, page: PageClass, recordId }
574
+ const recordCtx = record !== undefined ? { ...ctx, record } : ctx
575
+ const [panel, schemaData] = await Promise.all([
576
+ panelInfo(pilotiq, req, recordPageRoute),
577
+ resolveSchema(elements, recordCtx).then(metas => applyRoleHooks(pilotiq, user, 'view', metas, recordPageRoute)),
578
+ ])
579
+
580
+ return {
581
+ pageType: 'record-page' as const,
582
+ panel,
583
+ page: PageClass.toMeta(),
584
+ resource: { name: R.name, label: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
585
+ mode: 'record' as const,
586
+ recordId,
587
+ subPage: { slug: subPageSlug, label: PageClass.getLabel() },
588
+ basePath: cfg.path,
589
+ layout: cfg.layout,
590
+ schemaData,
591
+ notifications: consumeFlashedNotifications(req),
592
+ }
593
+ }