@pilotiq/pilotiq 0.1.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 (1409) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +11 -0
  3. package/CLAUDE.md +207 -0
  4. package/LICENSE +21 -0
  5. package/dist/Cluster.d.ts +56 -0
  6. package/dist/Cluster.d.ts.map +1 -0
  7. package/dist/Cluster.js +62 -0
  8. package/dist/Cluster.js.map +1 -0
  9. package/dist/Column.d.ts +378 -0
  10. package/dist/Column.d.ts.map +1 -0
  11. package/dist/Column.js +434 -0
  12. package/dist/Column.js.map +1 -0
  13. package/dist/Global.d.ts +123 -0
  14. package/dist/Global.d.ts.map +1 -0
  15. package/dist/Global.js +124 -0
  16. package/dist/Global.js.map +1 -0
  17. package/dist/Page.d.ts +90 -0
  18. package/dist/Page.d.ts.map +1 -0
  19. package/dist/Page.js +107 -0
  20. package/dist/Page.js.map +1 -0
  21. package/dist/Pilotiq.d.ts +505 -0
  22. package/dist/Pilotiq.d.ts.map +1 -0
  23. package/dist/Pilotiq.js +463 -0
  24. package/dist/Pilotiq.js.map +1 -0
  25. package/dist/PilotiqRegistry.d.ts +10 -0
  26. package/dist/PilotiqRegistry.d.ts.map +1 -0
  27. package/dist/PilotiqRegistry.js +32 -0
  28. package/dist/PilotiqRegistry.js.map +1 -0
  29. package/dist/PilotiqServiceProvider.d.ts +16 -0
  30. package/dist/PilotiqServiceProvider.d.ts.map +1 -0
  31. package/dist/PilotiqServiceProvider.js +57 -0
  32. package/dist/PilotiqServiceProvider.js.map +1 -0
  33. package/dist/RelationManager.d.ts +372 -0
  34. package/dist/RelationManager.d.ts.map +1 -0
  35. package/dist/RelationManager.js +342 -0
  36. package/dist/RelationManager.js.map +1 -0
  37. package/dist/RenderHook.d.ts +86 -0
  38. package/dist/RenderHook.d.ts.map +1 -0
  39. package/dist/RenderHook.js +116 -0
  40. package/dist/RenderHook.js.map +1 -0
  41. package/dist/Resource.d.ts +290 -0
  42. package/dist/Resource.d.ts.map +1 -0
  43. package/dist/Resource.js +362 -0
  44. package/dist/Resource.js.map +1 -0
  45. package/dist/RightPanel.d.ts +92 -0
  46. package/dist/RightPanel.d.ts.map +1 -0
  47. package/dist/RightPanel.js +61 -0
  48. package/dist/RightPanel.js.map +1 -0
  49. package/dist/Tab.d.ts +92 -0
  50. package/dist/Tab.d.ts.map +1 -0
  51. package/dist/Tab.js +93 -0
  52. package/dist/Tab.js.map +1 -0
  53. package/dist/UserMenuItem.d.ts +76 -0
  54. package/dist/UserMenuItem.d.ts.map +1 -0
  55. package/dist/UserMenuItem.js +87 -0
  56. package/dist/UserMenuItem.js.map +1 -0
  57. package/dist/actions/Action.d.ts +888 -0
  58. package/dist/actions/Action.d.ts.map +1 -0
  59. package/dist/actions/Action.js +1652 -0
  60. package/dist/actions/Action.js.map +1 -0
  61. package/dist/actions/ActionGroup.d.ts +85 -0
  62. package/dist/actions/ActionGroup.d.ts.map +1 -0
  63. package/dist/actions/ActionGroup.js +132 -0
  64. package/dist/actions/ActionGroup.js.map +1 -0
  65. package/dist/actions/attachFactory.d.ts +67 -0
  66. package/dist/actions/attachFactory.d.ts.map +1 -0
  67. package/dist/actions/attachFactory.js +115 -0
  68. package/dist/actions/attachFactory.js.map +1 -0
  69. package/dist/actions/exportFactory.d.ts +88 -0
  70. package/dist/actions/exportFactory.d.ts.map +1 -0
  71. package/dist/actions/exportFactory.js +144 -0
  72. package/dist/actions/exportFactory.js.map +1 -0
  73. package/dist/actions/importFactory.d.ts +97 -0
  74. package/dist/actions/importFactory.d.ts.map +1 -0
  75. package/dist/actions/importFactory.js +143 -0
  76. package/dist/actions/importFactory.js.map +1 -0
  77. package/dist/actions/index.d.ts +3 -0
  78. package/dist/actions/index.d.ts.map +1 -0
  79. package/dist/actions/index.js +3 -0
  80. package/dist/actions/index.js.map +1 -0
  81. package/dist/applyPageHooks.d.ts +54 -0
  82. package/dist/applyPageHooks.d.ts.map +1 -0
  83. package/dist/applyPageHooks.js +149 -0
  84. package/dist/applyPageHooks.js.map +1 -0
  85. package/dist/cells/coerce.d.ts +25 -0
  86. package/dist/cells/coerce.d.ts.map +1 -0
  87. package/dist/cells/coerce.js +87 -0
  88. package/dist/cells/coerce.js.map +1 -0
  89. package/dist/clusterPaths.d.ts +9 -0
  90. package/dist/clusterPaths.d.ts.map +1 -0
  91. package/dist/clusterPaths.js +19 -0
  92. package/dist/clusterPaths.js.map +1 -0
  93. package/dist/columns/BadgeColumn.d.ts +16 -0
  94. package/dist/columns/BadgeColumn.d.ts.map +1 -0
  95. package/dist/columns/BadgeColumn.js +25 -0
  96. package/dist/columns/BadgeColumn.js.map +1 -0
  97. package/dist/columns/BooleanColumn.d.ts +10 -0
  98. package/dist/columns/BooleanColumn.d.ts.map +1 -0
  99. package/dist/columns/BooleanColumn.js +18 -0
  100. package/dist/columns/BooleanColumn.js.map +1 -0
  101. package/dist/columns/ColorColumn.d.ts +27 -0
  102. package/dist/columns/ColorColumn.d.ts.map +1 -0
  103. package/dist/columns/ColorColumn.js +35 -0
  104. package/dist/columns/ColorColumn.js.map +1 -0
  105. package/dist/columns/IconColumn.d.ts +22 -0
  106. package/dist/columns/IconColumn.d.ts.map +1 -0
  107. package/dist/columns/IconColumn.js +28 -0
  108. package/dist/columns/IconColumn.js.map +1 -0
  109. package/dist/columns/ImageColumn.d.ts +17 -0
  110. package/dist/columns/ImageColumn.d.ts.map +1 -0
  111. package/dist/columns/ImageColumn.js +24 -0
  112. package/dist/columns/ImageColumn.js.map +1 -0
  113. package/dist/columns/SelectColumn.d.ts +36 -0
  114. package/dist/columns/SelectColumn.d.ts.map +1 -0
  115. package/dist/columns/SelectColumn.js +52 -0
  116. package/dist/columns/SelectColumn.js.map +1 -0
  117. package/dist/columns/TextColumn.d.ts +18 -0
  118. package/dist/columns/TextColumn.d.ts.map +1 -0
  119. package/dist/columns/TextColumn.js +20 -0
  120. package/dist/columns/TextColumn.js.map +1 -0
  121. package/dist/columns/TextInputColumn.d.ts +47 -0
  122. package/dist/columns/TextInputColumn.d.ts.map +1 -0
  123. package/dist/columns/TextInputColumn.js +60 -0
  124. package/dist/columns/TextInputColumn.js.map +1 -0
  125. package/dist/columns/ToggleColumn.d.ts +32 -0
  126. package/dist/columns/ToggleColumn.d.ts.map +1 -0
  127. package/dist/columns/ToggleColumn.js +45 -0
  128. package/dist/columns/ToggleColumn.js.map +1 -0
  129. package/dist/columns/index.d.ts +10 -0
  130. package/dist/columns/index.d.ts.map +1 -0
  131. package/dist/columns/index.js +10 -0
  132. package/dist/columns/index.js.map +1 -0
  133. package/dist/defaultGlobalPages.d.ts +11 -0
  134. package/dist/defaultGlobalPages.d.ts.map +1 -0
  135. package/dist/defaultGlobalPages.js +87 -0
  136. package/dist/defaultGlobalPages.js.map +1 -0
  137. package/dist/defaultPages.d.ts +247 -0
  138. package/dist/defaultPages.d.ts.map +1 -0
  139. package/dist/defaultPages.js +558 -0
  140. package/dist/defaultPages.js.map +1 -0
  141. package/dist/elements/Form.d.ts +219 -0
  142. package/dist/elements/Form.d.ts.map +1 -0
  143. package/dist/elements/Form.js +259 -0
  144. package/dist/elements/Form.js.map +1 -0
  145. package/dist/elements/ListTabs.d.ts +17 -0
  146. package/dist/elements/ListTabs.d.ts.map +1 -0
  147. package/dist/elements/ListTabs.js +23 -0
  148. package/dist/elements/ListTabs.js.map +1 -0
  149. package/dist/elements/Table.d.ts +535 -0
  150. package/dist/elements/Table.d.ts.map +1 -0
  151. package/dist/elements/Table.js +481 -0
  152. package/dist/elements/Table.js.map +1 -0
  153. package/dist/elements/TableGroup.d.ts +121 -0
  154. package/dist/elements/TableGroup.d.ts.map +1 -0
  155. package/dist/elements/TableGroup.js +162 -0
  156. package/dist/elements/TableGroup.js.map +1 -0
  157. package/dist/elements/dispatchAction.d.ts +127 -0
  158. package/dist/elements/dispatchAction.d.ts.map +1 -0
  159. package/dist/elements/dispatchAction.js +254 -0
  160. package/dist/elements/dispatchAction.js.map +1 -0
  161. package/dist/elements/dispatchForm.d.ts +220 -0
  162. package/dist/elements/dispatchForm.d.ts.map +1 -0
  163. package/dist/elements/dispatchForm.js +1645 -0
  164. package/dist/elements/dispatchForm.js.map +1 -0
  165. package/dist/elements/dispatchTable.d.ts +69 -0
  166. package/dist/elements/dispatchTable.d.ts.map +1 -0
  167. package/dist/elements/dispatchTable.js +606 -0
  168. package/dist/elements/dispatchTable.js.map +1 -0
  169. package/dist/elements/index.d.ts +3 -0
  170. package/dist/elements/index.d.ts.map +1 -0
  171. package/dist/elements/index.js +3 -0
  172. package/dist/elements/index.js.map +1 -0
  173. package/dist/entries/BadgeEntry.d.ts +21 -0
  174. package/dist/entries/BadgeEntry.d.ts.map +1 -0
  175. package/dist/entries/BadgeEntry.js +32 -0
  176. package/dist/entries/BadgeEntry.js.map +1 -0
  177. package/dist/entries/CodeEntry.d.ts +38 -0
  178. package/dist/entries/CodeEntry.d.ts.map +1 -0
  179. package/dist/entries/CodeEntry.js +44 -0
  180. package/dist/entries/CodeEntry.js.map +1 -0
  181. package/dist/entries/ColorEntry.d.ts +32 -0
  182. package/dist/entries/ColorEntry.d.ts.map +1 -0
  183. package/dist/entries/ColorEntry.js +48 -0
  184. package/dist/entries/ColorEntry.js.map +1 -0
  185. package/dist/entries/ComponentEntry.d.ts +66 -0
  186. package/dist/entries/ComponentEntry.d.ts.map +1 -0
  187. package/dist/entries/ComponentEntry.js +86 -0
  188. package/dist/entries/ComponentEntry.js.map +1 -0
  189. package/dist/entries/Entry.d.ts +175 -0
  190. package/dist/entries/Entry.d.ts.map +1 -0
  191. package/dist/entries/Entry.js +233 -0
  192. package/dist/entries/Entry.js.map +1 -0
  193. package/dist/entries/IconEntry.d.ts +30 -0
  194. package/dist/entries/IconEntry.d.ts.map +1 -0
  195. package/dist/entries/IconEntry.js +34 -0
  196. package/dist/entries/IconEntry.js.map +1 -0
  197. package/dist/entries/ImageEntry.d.ts +33 -0
  198. package/dist/entries/ImageEntry.d.ts.map +1 -0
  199. package/dist/entries/ImageEntry.js +47 -0
  200. package/dist/entries/ImageEntry.js.map +1 -0
  201. package/dist/entries/KeyValueEntry.d.ts +30 -0
  202. package/dist/entries/KeyValueEntry.d.ts.map +1 -0
  203. package/dist/entries/KeyValueEntry.js +38 -0
  204. package/dist/entries/KeyValueEntry.js.map +1 -0
  205. package/dist/entries/RepeatableEntry.d.ts +122 -0
  206. package/dist/entries/RepeatableEntry.d.ts.map +1 -0
  207. package/dist/entries/RepeatableEntry.js +121 -0
  208. package/dist/entries/RepeatableEntry.js.map +1 -0
  209. package/dist/entries/TextEntry.d.ts +38 -0
  210. package/dist/entries/TextEntry.d.ts.map +1 -0
  211. package/dist/entries/TextEntry.js +49 -0
  212. package/dist/entries/TextEntry.js.map +1 -0
  213. package/dist/entries/index.d.ts +2 -0
  214. package/dist/entries/index.d.ts.map +1 -0
  215. package/dist/entries/index.js +8 -0
  216. package/dist/entries/index.js.map +1 -0
  217. package/dist/entries/registry.d.ts +41 -0
  218. package/dist/entries/registry.d.ts.map +1 -0
  219. package/dist/entries/registry.js +17 -0
  220. package/dist/entries/registry.js.map +1 -0
  221. package/dist/fields/BuilderField.d.ts +420 -0
  222. package/dist/fields/BuilderField.d.ts.map +1 -0
  223. package/dist/fields/BuilderField.js +359 -0
  224. package/dist/fields/BuilderField.js.map +1 -0
  225. package/dist/fields/CheckboxField.d.ts +18 -0
  226. package/dist/fields/CheckboxField.d.ts.map +1 -0
  227. package/dist/fields/CheckboxField.js +23 -0
  228. package/dist/fields/CheckboxField.js.map +1 -0
  229. package/dist/fields/CheckboxListField.d.ts +25 -0
  230. package/dist/fields/CheckboxListField.d.ts.map +1 -0
  231. package/dist/fields/CheckboxListField.js +46 -0
  232. package/dist/fields/CheckboxListField.js.map +1 -0
  233. package/dist/fields/ColorPickerField.d.ts +16 -0
  234. package/dist/fields/ColorPickerField.d.ts.map +1 -0
  235. package/dist/fields/ColorPickerField.js +21 -0
  236. package/dist/fields/ColorPickerField.js.map +1 -0
  237. package/dist/fields/DateField.d.ts +29 -0
  238. package/dist/fields/DateField.d.ts.map +1 -0
  239. package/dist/fields/DateField.js +45 -0
  240. package/dist/fields/DateField.js.map +1 -0
  241. package/dist/fields/EmailField.d.ts +8 -0
  242. package/dist/fields/EmailField.d.ts.map +1 -0
  243. package/dist/fields/EmailField.js +13 -0
  244. package/dist/fields/EmailField.js.map +1 -0
  245. package/dist/fields/Field.d.ts +485 -0
  246. package/dist/fields/Field.d.ts.map +1 -0
  247. package/dist/fields/Field.js +539 -0
  248. package/dist/fields/Field.js.map +1 -0
  249. package/dist/fields/FileUploadField.d.ts +43 -0
  250. package/dist/fields/FileUploadField.d.ts.map +1 -0
  251. package/dist/fields/FileUploadField.js +60 -0
  252. package/dist/fields/FileUploadField.js.map +1 -0
  253. package/dist/fields/HiddenField.d.ts +19 -0
  254. package/dist/fields/HiddenField.d.ts.map +1 -0
  255. package/dist/fields/HiddenField.js +24 -0
  256. package/dist/fields/HiddenField.js.map +1 -0
  257. package/dist/fields/KeyValueField.d.ts +36 -0
  258. package/dist/fields/KeyValueField.d.ts.map +1 -0
  259. package/dist/fields/KeyValueField.js +47 -0
  260. package/dist/fields/KeyValueField.js.map +1 -0
  261. package/dist/fields/MarkdownField.d.ts +79 -0
  262. package/dist/fields/MarkdownField.d.ts.map +1 -0
  263. package/dist/fields/MarkdownField.js +117 -0
  264. package/dist/fields/MarkdownField.js.map +1 -0
  265. package/dist/fields/NumberField.d.ts +17 -0
  266. package/dist/fields/NumberField.d.ts.map +1 -0
  267. package/dist/fields/NumberField.js +27 -0
  268. package/dist/fields/NumberField.js.map +1 -0
  269. package/dist/fields/RadioField.d.ts +26 -0
  270. package/dist/fields/RadioField.d.ts.map +1 -0
  271. package/dist/fields/RadioField.js +47 -0
  272. package/dist/fields/RadioField.js.map +1 -0
  273. package/dist/fields/RepeaterField.d.ts +594 -0
  274. package/dist/fields/RepeaterField.d.ts.map +1 -0
  275. package/dist/fields/RepeaterField.js +504 -0
  276. package/dist/fields/RepeaterField.js.map +1 -0
  277. package/dist/fields/RowButton.d.ts +86 -0
  278. package/dist/fields/RowButton.d.ts.map +1 -0
  279. package/dist/fields/RowButton.js +85 -0
  280. package/dist/fields/RowButton.js.map +1 -0
  281. package/dist/fields/SelectField.d.ts +127 -0
  282. package/dist/fields/SelectField.d.ts.map +1 -0
  283. package/dist/fields/SelectField.js +160 -0
  284. package/dist/fields/SelectField.js.map +1 -0
  285. package/dist/fields/SliderField.d.ts +31 -0
  286. package/dist/fields/SliderField.d.ts.map +1 -0
  287. package/dist/fields/SliderField.js +45 -0
  288. package/dist/fields/SliderField.js.map +1 -0
  289. package/dist/fields/SlugField.d.ts +11 -0
  290. package/dist/fields/SlugField.d.ts.map +1 -0
  291. package/dist/fields/SlugField.js +19 -0
  292. package/dist/fields/SlugField.js.map +1 -0
  293. package/dist/fields/TagsInputField.d.ts +65 -0
  294. package/dist/fields/TagsInputField.d.ts.map +1 -0
  295. package/dist/fields/TagsInputField.js +104 -0
  296. package/dist/fields/TagsInputField.js.map +1 -0
  297. package/dist/fields/TextField.d.ts +11 -0
  298. package/dist/fields/TextField.d.ts.map +1 -0
  299. package/dist/fields/TextField.js +19 -0
  300. package/dist/fields/TextField.js.map +1 -0
  301. package/dist/fields/TextareaField.d.ts +40 -0
  302. package/dist/fields/TextareaField.d.ts.map +1 -0
  303. package/dist/fields/TextareaField.js +51 -0
  304. package/dist/fields/TextareaField.js.map +1 -0
  305. package/dist/fields/ToggleButtonsField.d.ts +24 -0
  306. package/dist/fields/ToggleButtonsField.d.ts.map +1 -0
  307. package/dist/fields/ToggleButtonsField.js +41 -0
  308. package/dist/fields/ToggleButtonsField.js.map +1 -0
  309. package/dist/fields/ToggleField.d.ts +8 -0
  310. package/dist/fields/ToggleField.d.ts.map +1 -0
  311. package/dist/fields/ToggleField.js +13 -0
  312. package/dist/fields/ToggleField.js.map +1 -0
  313. package/dist/fields/optionsResolver.d.ts +54 -0
  314. package/dist/fields/optionsResolver.d.ts.map +1 -0
  315. package/dist/fields/optionsResolver.js +62 -0
  316. package/dist/fields/optionsResolver.js.map +1 -0
  317. package/dist/fields/resolveField.d.ts +21 -0
  318. package/dist/fields/resolveField.d.ts.map +1 -0
  319. package/dist/fields/resolveField.js +26 -0
  320. package/dist/fields/resolveField.js.map +1 -0
  321. package/dist/filters/BooleanFilter.d.ts +20 -0
  322. package/dist/filters/BooleanFilter.d.ts.map +1 -0
  323. package/dist/filters/BooleanFilter.js +31 -0
  324. package/dist/filters/BooleanFilter.js.map +1 -0
  325. package/dist/filters/DateRangeFilter.d.ts +68 -0
  326. package/dist/filters/DateRangeFilter.d.ts.map +1 -0
  327. package/dist/filters/DateRangeFilter.js +137 -0
  328. package/dist/filters/DateRangeFilter.js.map +1 -0
  329. package/dist/filters/Filter.d.ts +140 -0
  330. package/dist/filters/Filter.d.ts.map +1 -0
  331. package/dist/filters/Filter.js +99 -0
  332. package/dist/filters/Filter.js.map +1 -0
  333. package/dist/filters/FormFilter.d.ts +103 -0
  334. package/dist/filters/FormFilter.d.ts.map +1 -0
  335. package/dist/filters/FormFilter.js +180 -0
  336. package/dist/filters/FormFilter.js.map +1 -0
  337. package/dist/filters/MultiSelectFilter.d.ts +41 -0
  338. package/dist/filters/MultiSelectFilter.d.ts.map +1 -0
  339. package/dist/filters/MultiSelectFilter.js +67 -0
  340. package/dist/filters/MultiSelectFilter.js.map +1 -0
  341. package/dist/filters/QueryBuilderFilter.d.ts +145 -0
  342. package/dist/filters/QueryBuilderFilter.d.ts.map +1 -0
  343. package/dist/filters/QueryBuilderFilter.js +323 -0
  344. package/dist/filters/QueryBuilderFilter.js.map +1 -0
  345. package/dist/filters/SelectFilter.d.ts +26 -0
  346. package/dist/filters/SelectFilter.d.ts.map +1 -0
  347. package/dist/filters/SelectFilter.js +35 -0
  348. package/dist/filters/SelectFilter.js.map +1 -0
  349. package/dist/filters/TernaryFilter.d.ts +35 -0
  350. package/dist/filters/TernaryFilter.d.ts.map +1 -0
  351. package/dist/filters/TernaryFilter.js +71 -0
  352. package/dist/filters/TernaryFilter.js.map +1 -0
  353. package/dist/filters/TrashedFilter.d.ts +28 -0
  354. package/dist/filters/TrashedFilter.d.ts.map +1 -0
  355. package/dist/filters/TrashedFilter.js +52 -0
  356. package/dist/filters/TrashedFilter.js.map +1 -0
  357. package/dist/filters/queryBuilder/BooleanConstraint.d.ts +13 -0
  358. package/dist/filters/queryBuilder/BooleanConstraint.d.ts.map +1 -0
  359. package/dist/filters/queryBuilder/BooleanConstraint.js +27 -0
  360. package/dist/filters/queryBuilder/BooleanConstraint.js.map +1 -0
  361. package/dist/filters/queryBuilder/Constraint.d.ts +74 -0
  362. package/dist/filters/queryBuilder/Constraint.d.ts.map +1 -0
  363. package/dist/filters/queryBuilder/Constraint.js +45 -0
  364. package/dist/filters/queryBuilder/Constraint.js.map +1 -0
  365. package/dist/filters/queryBuilder/DateConstraint.d.ts +18 -0
  366. package/dist/filters/queryBuilder/DateConstraint.d.ts.map +1 -0
  367. package/dist/filters/queryBuilder/DateConstraint.js +63 -0
  368. package/dist/filters/queryBuilder/DateConstraint.js.map +1 -0
  369. package/dist/filters/queryBuilder/NumberConstraint.d.ts +12 -0
  370. package/dist/filters/queryBuilder/NumberConstraint.d.ts.map +1 -0
  371. package/dist/filters/queryBuilder/NumberConstraint.js +61 -0
  372. package/dist/filters/queryBuilder/NumberConstraint.js.map +1 -0
  373. package/dist/filters/queryBuilder/SelectConstraint.d.ts +22 -0
  374. package/dist/filters/queryBuilder/SelectConstraint.d.ts.map +1 -0
  375. package/dist/filters/queryBuilder/SelectConstraint.js +66 -0
  376. package/dist/filters/queryBuilder/SelectConstraint.js.map +1 -0
  377. package/dist/filters/queryBuilder/TextConstraint.d.ts +18 -0
  378. package/dist/filters/queryBuilder/TextConstraint.d.ts.map +1 -0
  379. package/dist/filters/queryBuilder/TextConstraint.js +58 -0
  380. package/dist/filters/queryBuilder/TextConstraint.js.map +1 -0
  381. package/dist/filters/queryBuilder/index.d.ts +7 -0
  382. package/dist/filters/queryBuilder/index.d.ts.map +1 -0
  383. package/dist/filters/queryBuilder/index.js +7 -0
  384. package/dist/filters/queryBuilder/index.js.map +1 -0
  385. package/dist/icons/index.d.ts +3 -0
  386. package/dist/icons/index.d.ts.map +1 -0
  387. package/dist/icons/index.js +3 -0
  388. package/dist/icons/index.js.map +1 -0
  389. package/dist/icons/lucide.d.ts +16 -0
  390. package/dist/icons/lucide.d.ts.map +1 -0
  391. package/dist/icons/lucide.js +173 -0
  392. package/dist/icons/lucide.js.map +1 -0
  393. package/dist/icons/registry.d.ts +27 -0
  394. package/dist/icons/registry.d.ts.map +1 -0
  395. package/dist/icons/registry.js +35 -0
  396. package/dist/icons/registry.js.map +1 -0
  397. package/dist/icons/types.d.ts +38 -0
  398. package/dist/icons/types.d.ts.map +1 -0
  399. package/dist/icons/types.js +23 -0
  400. package/dist/icons/types.js.map +1 -0
  401. package/dist/index.d.ts +118 -0
  402. package/dist/index.d.ts.map +1 -0
  403. package/dist/index.js +135 -0
  404. package/dist/index.js.map +1 -0
  405. package/dist/io/csv.d.ts +51 -0
  406. package/dist/io/csv.d.ts.map +1 -0
  407. package/dist/io/csv.js +168 -0
  408. package/dist/io/csv.js.map +1 -0
  409. package/dist/notifications/Notification.d.ts +181 -0
  410. package/dist/notifications/Notification.d.ts.map +1 -0
  411. package/dist/notifications/Notification.js +290 -0
  412. package/dist/notifications/Notification.js.map +1 -0
  413. package/dist/notifications/broadcast.d.ts +58 -0
  414. package/dist/notifications/broadcast.d.ts.map +1 -0
  415. package/dist/notifications/broadcast.js +72 -0
  416. package/dist/notifications/broadcast.js.map +1 -0
  417. package/dist/notifications/database.d.ts +164 -0
  418. package/dist/notifications/database.d.ts.map +1 -0
  419. package/dist/notifications/database.js +321 -0
  420. package/dist/notifications/database.js.map +1 -0
  421. package/dist/notifications/dispatchNotificationAction.d.ts +48 -0
  422. package/dist/notifications/dispatchNotificationAction.d.ts.map +1 -0
  423. package/dist/notifications/dispatchNotificationAction.js +100 -0
  424. package/dist/notifications/dispatchNotificationAction.js.map +1 -0
  425. package/dist/notifications/flash.d.ts +34 -0
  426. package/dist/notifications/flash.d.ts.map +1 -0
  427. package/dist/notifications/flash.js +51 -0
  428. package/dist/notifications/flash.js.map +1 -0
  429. package/dist/notifications/index.d.ts +8 -0
  430. package/dist/notifications/index.d.ts.map +1 -0
  431. package/dist/notifications/index.js +6 -0
  432. package/dist/notifications/index.js.map +1 -0
  433. package/dist/notifications/registerBroadcastAuth.d.ts +45 -0
  434. package/dist/notifications/registerBroadcastAuth.d.ts.map +1 -0
  435. package/dist/notifications/registerBroadcastAuth.js +86 -0
  436. package/dist/notifications/registerBroadcastAuth.js.map +1 -0
  437. package/dist/notifications/resolveSavedNotification.d.ts +21 -0
  438. package/dist/notifications/resolveSavedNotification.d.ts.map +1 -0
  439. package/dist/notifications/resolveSavedNotification.js +43 -0
  440. package/dist/notifications/resolveSavedNotification.js.map +1 -0
  441. package/dist/notifications/types.d.ts +87 -0
  442. package/dist/notifications/types.d.ts.map +1 -0
  443. package/dist/notifications/types.js +2 -0
  444. package/dist/notifications/types.js.map +1 -0
  445. package/dist/orm/m2mAccessor.d.ts +49 -0
  446. package/dist/orm/m2mAccessor.d.ts.map +1 -0
  447. package/dist/orm/m2mAccessor.js +45 -0
  448. package/dist/orm/m2mAccessor.js.map +1 -0
  449. package/dist/orm/modelDefaults.d.ts +347 -0
  450. package/dist/orm/modelDefaults.d.ts.map +1 -0
  451. package/dist/orm/modelDefaults.js +375 -0
  452. package/dist/orm/modelDefaults.js.map +1 -0
  453. package/dist/pageData.d.ts +778 -0
  454. package/dist/pageData.d.ts.map +1 -0
  455. package/dist/pageData.js +3725 -0
  456. package/dist/pageData.js.map +1 -0
  457. package/dist/plugins/index.d.ts +2 -0
  458. package/dist/plugins/index.d.ts.map +1 -0
  459. package/dist/plugins/index.js +2 -0
  460. package/dist/plugins/index.js.map +1 -0
  461. package/dist/plugins/themeEditor.d.ts +17 -0
  462. package/dist/plugins/themeEditor.d.ts.map +1 -0
  463. package/dist/plugins/themeEditor.js +23 -0
  464. package/dist/plugins/themeEditor.js.map +1 -0
  465. package/dist/react/AppShell.d.ts +58 -0
  466. package/dist/react/AppShell.d.ts.map +1 -0
  467. package/dist/react/AppShell.js +58 -0
  468. package/dist/react/AppShell.js.map +1 -0
  469. package/dist/react/CommandPalette.d.ts +21 -0
  470. package/dist/react/CommandPalette.d.ts.map +1 -0
  471. package/dist/react/CommandPalette.js +236 -0
  472. package/dist/react/CommandPalette.js.map +1 -0
  473. package/dist/react/FormStateContext.d.ts +83 -0
  474. package/dist/react/FormStateContext.d.ts.map +1 -0
  475. package/dist/react/FormStateContext.js +284 -0
  476. package/dist/react/FormStateContext.js.map +1 -0
  477. package/dist/react/HeadHooks.d.ts +26 -0
  478. package/dist/react/HeadHooks.d.ts.map +1 -0
  479. package/dist/react/HeadHooks.js +141 -0
  480. package/dist/react/HeadHooks.js.map +1 -0
  481. package/dist/react/NotificationActionStrip.d.ts +39 -0
  482. package/dist/react/NotificationActionStrip.d.ts.map +1 -0
  483. package/dist/react/NotificationActionStrip.js +129 -0
  484. package/dist/react/NotificationActionStrip.js.map +1 -0
  485. package/dist/react/NotificationBell.d.ts +20 -0
  486. package/dist/react/NotificationBell.d.ts.map +1 -0
  487. package/dist/react/NotificationBell.js +273 -0
  488. package/dist/react/NotificationBell.js.map +1 -0
  489. package/dist/react/RenderHookSlot.d.ts +20 -0
  490. package/dist/react/RenderHookSlot.d.ts.map +1 -0
  491. package/dist/react/RenderHookSlot.js +24 -0
  492. package/dist/react/RenderHookSlot.js.map +1 -0
  493. package/dist/react/RightSidebar.d.ts +33 -0
  494. package/dist/react/RightSidebar.d.ts.map +1 -0
  495. package/dist/react/RightSidebar.js +82 -0
  496. package/dist/react/RightSidebar.js.map +1 -0
  497. package/dist/react/RightSidebarContext.d.ts +62 -0
  498. package/dist/react/RightSidebarContext.d.ts.map +1 -0
  499. package/dist/react/RightSidebarContext.js +178 -0
  500. package/dist/react/RightSidebarContext.js.map +1 -0
  501. package/dist/react/RightSidebarTrigger.d.ts +16 -0
  502. package/dist/react/RightSidebarTrigger.d.ts.map +1 -0
  503. package/dist/react/RightSidebarTrigger.js +24 -0
  504. package/dist/react/RightSidebarTrigger.js.map +1 -0
  505. package/dist/react/SchemaRenderer.d.ts +63 -0
  506. package/dist/react/SchemaRenderer.d.ts.map +1 -0
  507. package/dist/react/SchemaRenderer.js +3458 -0
  508. package/dist/react/SchemaRenderer.js.map +1 -0
  509. package/dist/react/SearchTrigger.d.ts +13 -0
  510. package/dist/react/SearchTrigger.d.ts.map +1 -0
  511. package/dist/react/SearchTrigger.js +30 -0
  512. package/dist/react/SearchTrigger.js.map +1 -0
  513. package/dist/react/ThemeProvider.d.ts +18 -0
  514. package/dist/react/ThemeProvider.d.ts.map +1 -0
  515. package/dist/react/ThemeProvider.js +66 -0
  516. package/dist/react/ThemeProvider.js.map +1 -0
  517. package/dist/react/ThemeSettingsPage.d.ts +10 -0
  518. package/dist/react/ThemeSettingsPage.d.ts.map +1 -0
  519. package/dist/react/ThemeSettingsPage.js +293 -0
  520. package/dist/react/ThemeSettingsPage.js.map +1 -0
  521. package/dist/react/ThemeToggle.d.ts +2 -0
  522. package/dist/react/ThemeToggle.d.ts.map +1 -0
  523. package/dist/react/ThemeToggle.js +8 -0
  524. package/dist/react/ThemeToggle.js.map +1 -0
  525. package/dist/react/Toaster.d.ts +25 -0
  526. package/dist/react/Toaster.d.ts.map +1 -0
  527. package/dist/react/Toaster.js +89 -0
  528. package/dist/react/Toaster.js.map +1 -0
  529. package/dist/react/UserMenu.d.ts +23 -0
  530. package/dist/react/UserMenu.d.ts.map +1 -0
  531. package/dist/react/UserMenu.js +78 -0
  532. package/dist/react/UserMenu.js.map +1 -0
  533. package/dist/react/WidgetDataContext.d.ts +64 -0
  534. package/dist/react/WidgetDataContext.d.ts.map +1 -0
  535. package/dist/react/WidgetDataContext.js +89 -0
  536. package/dist/react/WidgetDataContext.js.map +1 -0
  537. package/dist/react/cells/EditableCell.d.ts +20 -0
  538. package/dist/react/cells/EditableCell.d.ts.map +1 -0
  539. package/dist/react/cells/EditableCell.js +251 -0
  540. package/dist/react/cells/EditableCell.js.map +1 -0
  541. package/dist/react/fieldJsHandler.d.ts +33 -0
  542. package/dist/react/fieldJsHandler.d.ts.map +1 -0
  543. package/dist/react/fieldJsHandler.js +61 -0
  544. package/dist/react/fieldJsHandler.js.map +1 -0
  545. package/dist/react/fields/BuilderInput.d.ts +21 -0
  546. package/dist/react/fields/BuilderInput.d.ts.map +1 -0
  547. package/dist/react/fields/BuilderInput.js +553 -0
  548. package/dist/react/fields/BuilderInput.js.map +1 -0
  549. package/dist/react/fields/CheckboxInput.d.ts +9 -0
  550. package/dist/react/fields/CheckboxInput.d.ts.map +1 -0
  551. package/dist/react/fields/CheckboxInput.js +23 -0
  552. package/dist/react/fields/CheckboxInput.js.map +1 -0
  553. package/dist/react/fields/CheckboxListInput.d.ts +19 -0
  554. package/dist/react/fields/CheckboxListInput.d.ts.map +1 -0
  555. package/dist/react/fields/CheckboxListInput.js +53 -0
  556. package/dist/react/fields/CheckboxListInput.js.map +1 -0
  557. package/dist/react/fields/ColorInput.d.ts +12 -0
  558. package/dist/react/fields/ColorInput.d.ts.map +1 -0
  559. package/dist/react/fields/ColorInput.js +29 -0
  560. package/dist/react/fields/ColorInput.js.map +1 -0
  561. package/dist/react/fields/DateFieldInput.d.ts +8 -0
  562. package/dist/react/fields/DateFieldInput.d.ts.map +1 -0
  563. package/dist/react/fields/DateFieldInput.js +39 -0
  564. package/dist/react/fields/DateFieldInput.js.map +1 -0
  565. package/dist/react/fields/DateTimeInput.d.ts +13 -0
  566. package/dist/react/fields/DateTimeInput.d.ts.map +1 -0
  567. package/dist/react/fields/DateTimeInput.js +29 -0
  568. package/dist/react/fields/DateTimeInput.js.map +1 -0
  569. package/dist/react/fields/FieldShell.d.ts +23 -0
  570. package/dist/react/fields/FieldShell.d.ts.map +1 -0
  571. package/dist/react/fields/FieldShell.js +46 -0
  572. package/dist/react/fields/FieldShell.js.map +1 -0
  573. package/dist/react/fields/FileUploadInput.d.ts +21 -0
  574. package/dist/react/fields/FileUploadInput.d.ts.map +1 -0
  575. package/dist/react/fields/FileUploadInput.js +120 -0
  576. package/dist/react/fields/FileUploadInput.js.map +1 -0
  577. package/dist/react/fields/HiddenInput.d.ts +11 -0
  578. package/dist/react/fields/HiddenInput.d.ts.map +1 -0
  579. package/dist/react/fields/HiddenInput.js +14 -0
  580. package/dist/react/fields/HiddenInput.js.map +1 -0
  581. package/dist/react/fields/KeyValueInput.d.ts +18 -0
  582. package/dist/react/fields/KeyValueInput.d.ts.map +1 -0
  583. package/dist/react/fields/KeyValueInput.js +122 -0
  584. package/dist/react/fields/KeyValueInput.js.map +1 -0
  585. package/dist/react/fields/MarkdownInput.d.ts +29 -0
  586. package/dist/react/fields/MarkdownInput.d.ts.map +1 -0
  587. package/dist/react/fields/MarkdownInput.js +250 -0
  588. package/dist/react/fields/MarkdownInput.js.map +1 -0
  589. package/dist/react/fields/RadioInput.d.ts +18 -0
  590. package/dist/react/fields/RadioInput.d.ts.map +1 -0
  591. package/dist/react/fields/RadioInput.js +34 -0
  592. package/dist/react/fields/RadioInput.js.map +1 -0
  593. package/dist/react/fields/RepeaterInput.d.ts +92 -0
  594. package/dist/react/fields/RepeaterInput.d.ts.map +1 -0
  595. package/dist/react/fields/RepeaterInput.js +705 -0
  596. package/dist/react/fields/RepeaterInput.js.map +1 -0
  597. package/dist/react/fields/SelectFieldInput.d.ts +23 -0
  598. package/dist/react/fields/SelectFieldInput.d.ts.map +1 -0
  599. package/dist/react/fields/SelectFieldInput.js +146 -0
  600. package/dist/react/fields/SelectFieldInput.js.map +1 -0
  601. package/dist/react/fields/SliderInput.d.ts +16 -0
  602. package/dist/react/fields/SliderInput.d.ts.map +1 -0
  603. package/dist/react/fields/SliderInput.js +37 -0
  604. package/dist/react/fields/SliderInput.js.map +1 -0
  605. package/dist/react/fields/TagsInput.d.ts +27 -0
  606. package/dist/react/fields/TagsInput.d.ts.map +1 -0
  607. package/dist/react/fields/TagsInput.js +189 -0
  608. package/dist/react/fields/TagsInput.js.map +1 -0
  609. package/dist/react/fields/TextLikeInput.d.ts +18 -0
  610. package/dist/react/fields/TextLikeInput.d.ts.map +1 -0
  611. package/dist/react/fields/TextLikeInput.js +46 -0
  612. package/dist/react/fields/TextLikeInput.js.map +1 -0
  613. package/dist/react/fields/ToggleButtonsInput.d.ts +20 -0
  614. package/dist/react/fields/ToggleButtonsInput.d.ts.map +1 -0
  615. package/dist/react/fields/ToggleButtonsInput.js +42 -0
  616. package/dist/react/fields/ToggleButtonsInput.js.map +1 -0
  617. package/dist/react/fields/ToggleFieldInput.d.ts +7 -0
  618. package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -0
  619. package/dist/react/fields/ToggleFieldInput.js +30 -0
  620. package/dist/react/fields/ToggleFieldInput.js.map +1 -0
  621. package/dist/react/fields/rowChromeButton.d.ts +84 -0
  622. package/dist/react/fields/rowChromeButton.d.ts.map +1 -0
  623. package/dist/react/fields/rowChromeButton.js +111 -0
  624. package/dist/react/fields/rowChromeButton.js.map +1 -0
  625. package/dist/react/fields/syncRowGates.d.ts +11 -0
  626. package/dist/react/fields/syncRowGates.d.ts.map +1 -0
  627. package/dist/react/fields/syncRowGates.js +55 -0
  628. package/dist/react/fields/syncRowGates.js.map +1 -0
  629. package/dist/react/formStateHelpers.d.ts +44 -0
  630. package/dist/react/formStateHelpers.d.ts.map +1 -0
  631. package/dist/react/formStateHelpers.js +230 -0
  632. package/dist/react/formStateHelpers.js.map +1 -0
  633. package/dist/react/hooks/use-mobile.d.ts +2 -0
  634. package/dist/react/hooks/use-mobile.d.ts.map +1 -0
  635. package/dist/react/hooks/use-mobile.js +16 -0
  636. package/dist/react/hooks/use-mobile.js.map +1 -0
  637. package/dist/react/icon-context.d.ts +35 -0
  638. package/dist/react/icon-context.d.ts.map +1 -0
  639. package/dist/react/icon-context.js +45 -0
  640. package/dist/react/icon-context.js.map +1 -0
  641. package/dist/react/index.d.ts +26 -0
  642. package/dist/react/index.d.ts.map +1 -0
  643. package/dist/react/index.js +28 -0
  644. package/dist/react/index.js.map +1 -0
  645. package/dist/react/layouts/SidebarLayout.d.ts +3 -0
  646. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -0
  647. package/dist/react/layouts/SidebarLayout.js +85 -0
  648. package/dist/react/layouts/SidebarLayout.js.map +1 -0
  649. package/dist/react/layouts/TopbarLayout.d.ts +3 -0
  650. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -0
  651. package/dist/react/layouts/TopbarLayout.js +103 -0
  652. package/dist/react/layouts/TopbarLayout.js.map +1 -0
  653. package/dist/react/navigate.d.ts +22 -0
  654. package/dist/react/navigate.d.ts.map +1 -0
  655. package/dist/react/navigate.js +30 -0
  656. package/dist/react/navigate.js.map +1 -0
  657. package/dist/react/registry.d.ts +35 -0
  658. package/dist/react/registry.d.ts.map +1 -0
  659. package/dist/react/registry.js +22 -0
  660. package/dist/react/registry.js.map +1 -0
  661. package/dist/react/right-panel-registry.d.ts +32 -0
  662. package/dist/react/right-panel-registry.d.ts.map +1 -0
  663. package/dist/react/right-panel-registry.js +20 -0
  664. package/dist/react/right-panel-registry.js.map +1 -0
  665. package/dist/react/theme-preview/apply.d.ts +11 -0
  666. package/dist/react/theme-preview/apply.d.ts.map +1 -0
  667. package/dist/react/theme-preview/apply.js +93 -0
  668. package/dist/react/theme-preview/apply.js.map +1 -0
  669. package/dist/react/theme-preview/build-html.d.ts +3 -0
  670. package/dist/react/theme-preview/build-html.d.ts.map +1 -0
  671. package/dist/react/theme-preview/build-html.js +437 -0
  672. package/dist/react/theme-preview/build-html.js.map +1 -0
  673. package/dist/react/ui/button.d.ts +9 -0
  674. package/dist/react/ui/button.d.ts.map +1 -0
  675. package/dist/react/ui/button.js +35 -0
  676. package/dist/react/ui/button.js.map +1 -0
  677. package/dist/react/ui/calendar.d.ts +5 -0
  678. package/dist/react/ui/calendar.d.ts.map +1 -0
  679. package/dist/react/ui/calendar.js +34 -0
  680. package/dist/react/ui/calendar.js.map +1 -0
  681. package/dist/react/ui/checkbox.d.ts +4 -0
  682. package/dist/react/ui/checkbox.d.ts.map +1 -0
  683. package/dist/react/ui/checkbox.js +9 -0
  684. package/dist/react/ui/checkbox.js.map +1 -0
  685. package/dist/react/ui/dialog.d.ts +12 -0
  686. package/dist/react/ui/dialog.d.ts.map +1 -0
  687. package/dist/react/ui/dialog.js +34 -0
  688. package/dist/react/ui/dialog.js.map +1 -0
  689. package/dist/react/ui/dropdown-menu.d.ts +12 -0
  690. package/dist/react/ui/dropdown-menu.d.ts.map +1 -0
  691. package/dist/react/ui/dropdown-menu.js +23 -0
  692. package/dist/react/ui/dropdown-menu.js.map +1 -0
  693. package/dist/react/ui/input.d.ts +4 -0
  694. package/dist/react/ui/input.d.ts.map +1 -0
  695. package/dist/react/ui/input.js +8 -0
  696. package/dist/react/ui/input.js.map +1 -0
  697. package/dist/react/ui/label.d.ts +4 -0
  698. package/dist/react/ui/label.d.ts.map +1 -0
  699. package/dist/react/ui/label.js +7 -0
  700. package/dist/react/ui/label.js.map +1 -0
  701. package/dist/react/ui/popover.d.ts +6 -0
  702. package/dist/react/ui/popover.d.ts.map +1 -0
  703. package/dist/react/ui/popover.js +14 -0
  704. package/dist/react/ui/popover.js.map +1 -0
  705. package/dist/react/ui/select.d.ts +17 -0
  706. package/dist/react/ui/select.d.ts.map +1 -0
  707. package/dist/react/ui/select.js +39 -0
  708. package/dist/react/ui/select.js.map +1 -0
  709. package/dist/react/ui/separator.d.ts +4 -0
  710. package/dist/react/ui/separator.d.ts.map +1 -0
  711. package/dist/react/ui/separator.js +9 -0
  712. package/dist/react/ui/separator.js.map +1 -0
  713. package/dist/react/ui/sheet.d.ts +15 -0
  714. package/dist/react/ui/sheet.d.ts.map +1 -0
  715. package/dist/react/ui/sheet.js +37 -0
  716. package/dist/react/ui/sheet.js.map +1 -0
  717. package/dist/react/ui/sidebar.d.ts +64 -0
  718. package/dist/react/ui/sidebar.d.ts.map +1 -0
  719. package/dist/react/ui/sidebar.js +257 -0
  720. package/dist/react/ui/sidebar.js.map +1 -0
  721. package/dist/react/ui/skeleton.d.ts +3 -0
  722. package/dist/react/ui/skeleton.d.ts.map +1 -0
  723. package/dist/react/ui/skeleton.js +7 -0
  724. package/dist/react/ui/skeleton.js.map +1 -0
  725. package/dist/react/ui/slider.d.ts +4 -0
  726. package/dist/react/ui/slider.d.ts.map +1 -0
  727. package/dist/react/ui/slider.js +8 -0
  728. package/dist/react/ui/slider.js.map +1 -0
  729. package/dist/react/ui/switch.d.ts +4 -0
  730. package/dist/react/ui/switch.d.ts.map +1 -0
  731. package/dist/react/ui/switch.js +8 -0
  732. package/dist/react/ui/switch.js.map +1 -0
  733. package/dist/react/ui/table.d.ts +11 -0
  734. package/dist/react/ui/table.d.ts.map +1 -0
  735. package/dist/react/ui/table.js +28 -0
  736. package/dist/react/ui/table.js.map +1 -0
  737. package/dist/react/ui/tabs.d.ts +7 -0
  738. package/dist/react/ui/tabs.d.ts.map +1 -0
  739. package/dist/react/ui/tabs.js +17 -0
  740. package/dist/react/ui/tabs.js.map +1 -0
  741. package/dist/react/ui/textarea.d.ts +4 -0
  742. package/dist/react/ui/textarea.d.ts.map +1 -0
  743. package/dist/react/ui/textarea.js +7 -0
  744. package/dist/react/ui/textarea.js.map +1 -0
  745. package/dist/react/ui/tooltip.d.ts +7 -0
  746. package/dist/react/ui/tooltip.d.ts.map +1 -0
  747. package/dist/react/ui/tooltip.js +17 -0
  748. package/dist/react/ui/tooltip.js.map +1 -0
  749. package/dist/react/useResizableWidth.d.ts +47 -0
  750. package/dist/react/useResizableWidth.d.ts.map +1 -0
  751. package/dist/react/useResizableWidth.js +99 -0
  752. package/dist/react/useResizableWidth.js.map +1 -0
  753. package/dist/react/utils.d.ts +3 -0
  754. package/dist/react/utils.d.ts.map +1 -0
  755. package/dist/react/utils.js +6 -0
  756. package/dist/react/utils.js.map +1 -0
  757. package/dist/react/widgetRegistry.d.ts +33 -0
  758. package/dist/react/widgetRegistry.d.ts.map +1 -0
  759. package/dist/react/widgetRegistry.js +15 -0
  760. package/dist/react/widgetRegistry.js.map +1 -0
  761. package/dist/react/widgets/StatsOverviewRenderer.d.ts +6 -0
  762. package/dist/react/widgets/StatsOverviewRenderer.d.ts.map +1 -0
  763. package/dist/react/widgets/StatsOverviewRenderer.js +124 -0
  764. package/dist/react/widgets/StatsOverviewRenderer.js.map +1 -0
  765. package/dist/react/widgets/TableWidgetRenderer.d.ts +6 -0
  766. package/dist/react/widgets/TableWidgetRenderer.d.ts.map +1 -0
  767. package/dist/react/widgets/TableWidgetRenderer.js +123 -0
  768. package/dist/react/widgets/TableWidgetRenderer.js.map +1 -0
  769. package/dist/react/widgets/ViewRenderer.d.ts +16 -0
  770. package/dist/react/widgets/ViewRenderer.d.ts.map +1 -0
  771. package/dist/react/widgets/ViewRenderer.js +26 -0
  772. package/dist/react/widgets/ViewRenderer.js.map +1 -0
  773. package/dist/richtext/index.d.ts +2 -0
  774. package/dist/richtext/index.d.ts.map +1 -0
  775. package/dist/richtext/index.js +2 -0
  776. package/dist/richtext/index.js.map +1 -0
  777. package/dist/richtext/registry.d.ts +55 -0
  778. package/dist/richtext/registry.d.ts.map +1 -0
  779. package/dist/richtext/registry.js +66 -0
  780. package/dist/richtext/registry.js.map +1 -0
  781. package/dist/routes.d.ts +9 -0
  782. package/dist/routes.d.ts.map +1 -0
  783. package/dist/routes.js +3116 -0
  784. package/dist/routes.js.map +1 -0
  785. package/dist/schema/Alert.d.ts +33 -0
  786. package/dist/schema/Alert.d.ts.map +1 -0
  787. package/dist/schema/Alert.js +41 -0
  788. package/dist/schema/Alert.js.map +1 -0
  789. package/dist/schema/Block.d.ts +112 -0
  790. package/dist/schema/Block.d.ts.map +1 -0
  791. package/dist/schema/Block.js +136 -0
  792. package/dist/schema/Block.js.map +1 -0
  793. package/dist/schema/Breadcrumbs.d.ts +31 -0
  794. package/dist/schema/Breadcrumbs.d.ts.map +1 -0
  795. package/dist/schema/Breadcrumbs.js +30 -0
  796. package/dist/schema/Breadcrumbs.js.map +1 -0
  797. package/dist/schema/Card.d.ts +17 -0
  798. package/dist/schema/Card.d.ts.map +1 -0
  799. package/dist/schema/Card.js +31 -0
  800. package/dist/schema/Card.js.map +1 -0
  801. package/dist/schema/Divider.d.ts +12 -0
  802. package/dist/schema/Divider.d.ts.map +1 -0
  803. package/dist/schema/Divider.js +19 -0
  804. package/dist/schema/Divider.js.map +1 -0
  805. package/dist/schema/Element.d.ts +150 -0
  806. package/dist/schema/Element.d.ts.map +1 -0
  807. package/dist/schema/Element.js +124 -0
  808. package/dist/schema/Element.js.map +1 -0
  809. package/dist/schema/EmptyState.d.ts +48 -0
  810. package/dist/schema/EmptyState.d.ts.map +1 -0
  811. package/dist/schema/EmptyState.js +57 -0
  812. package/dist/schema/EmptyState.js.map +1 -0
  813. package/dist/schema/Fieldset.d.ts +25 -0
  814. package/dist/schema/Fieldset.d.ts.map +1 -0
  815. package/dist/schema/Fieldset.js +39 -0
  816. package/dist/schema/Fieldset.js.map +1 -0
  817. package/dist/schema/Grid.d.ts +23 -0
  818. package/dist/schema/Grid.d.ts.map +1 -0
  819. package/dist/schema/Grid.js +36 -0
  820. package/dist/schema/Grid.js.map +1 -0
  821. package/dist/schema/Group.d.ts +19 -0
  822. package/dist/schema/Group.d.ts.map +1 -0
  823. package/dist/schema/Group.js +26 -0
  824. package/dist/schema/Group.js.map +1 -0
  825. package/dist/schema/Heading.d.ts +25 -0
  826. package/dist/schema/Heading.d.ts.map +1 -0
  827. package/dist/schema/Heading.js +34 -0
  828. package/dist/schema/Heading.js.map +1 -0
  829. package/dist/schema/Html.d.ts +48 -0
  830. package/dist/schema/Html.d.ts.map +1 -0
  831. package/dist/schema/Html.js +60 -0
  832. package/dist/schema/Html.js.map +1 -0
  833. package/dist/schema/Icon.d.ts +34 -0
  834. package/dist/schema/Icon.d.ts.map +1 -0
  835. package/dist/schema/Icon.js +40 -0
  836. package/dist/schema/Icon.js.map +1 -0
  837. package/dist/schema/Image.d.ts +38 -0
  838. package/dist/schema/Image.d.ts.map +1 -0
  839. package/dist/schema/Image.js +48 -0
  840. package/dist/schema/Image.js.map +1 -0
  841. package/dist/schema/LinkTag.d.ts +48 -0
  842. package/dist/schema/LinkTag.d.ts.map +1 -0
  843. package/dist/schema/LinkTag.js +16 -0
  844. package/dist/schema/LinkTag.js.map +1 -0
  845. package/dist/schema/Markdown.d.ts +57 -0
  846. package/dist/schema/Markdown.d.ts.map +1 -0
  847. package/dist/schema/Markdown.js +75 -0
  848. package/dist/schema/Markdown.js.map +1 -0
  849. package/dist/schema/MetaTag.d.ts +41 -0
  850. package/dist/schema/MetaTag.d.ts.map +1 -0
  851. package/dist/schema/MetaTag.js +16 -0
  852. package/dist/schema/MetaTag.js.map +1 -0
  853. package/dist/schema/RelationTabs.d.ts +50 -0
  854. package/dist/schema/RelationTabs.d.ts.map +1 -0
  855. package/dist/schema/RelationTabs.js +48 -0
  856. package/dist/schema/RelationTabs.js.map +1 -0
  857. package/dist/schema/ScriptTag.d.ts +63 -0
  858. package/dist/schema/ScriptTag.d.ts.map +1 -0
  859. package/dist/schema/ScriptTag.js +16 -0
  860. package/dist/schema/ScriptTag.js.map +1 -0
  861. package/dist/schema/Section.d.ts +93 -0
  862. package/dist/schema/Section.d.ts.map +1 -0
  863. package/dist/schema/Section.js +127 -0
  864. package/dist/schema/Section.js.map +1 -0
  865. package/dist/schema/ServerDataElement.d.ts +101 -0
  866. package/dist/schema/ServerDataElement.d.ts.map +1 -0
  867. package/dist/schema/ServerDataElement.js +135 -0
  868. package/dist/schema/ServerDataElement.js.map +1 -0
  869. package/dist/schema/Split.d.ts +31 -0
  870. package/dist/schema/Split.d.ts.map +1 -0
  871. package/dist/schema/Split.js +41 -0
  872. package/dist/schema/Split.js.map +1 -0
  873. package/dist/schema/Stat.d.ts +92 -0
  874. package/dist/schema/Stat.d.ts.map +1 -0
  875. package/dist/schema/Stat.js +116 -0
  876. package/dist/schema/Stat.js.map +1 -0
  877. package/dist/schema/StatsOverview.d.ts +76 -0
  878. package/dist/schema/StatsOverview.d.ts.map +1 -0
  879. package/dist/schema/StatsOverview.js +71 -0
  880. package/dist/schema/StatsOverview.js.map +1 -0
  881. package/dist/schema/StyleTag.d.ts +32 -0
  882. package/dist/schema/StyleTag.d.ts.map +1 -0
  883. package/dist/schema/StyleTag.js +38 -0
  884. package/dist/schema/StyleTag.js.map +1 -0
  885. package/dist/schema/TableWidget.d.ts +148 -0
  886. package/dist/schema/TableWidget.d.ts.map +1 -0
  887. package/dist/schema/TableWidget.js +190 -0
  888. package/dist/schema/TableWidget.js.map +1 -0
  889. package/dist/schema/Tabs.d.ts +40 -0
  890. package/dist/schema/Tabs.d.ts.map +1 -0
  891. package/dist/schema/Tabs.js +66 -0
  892. package/dist/schema/Tabs.js.map +1 -0
  893. package/dist/schema/Text.d.ts +33 -0
  894. package/dist/schema/Text.d.ts.map +1 -0
  895. package/dist/schema/Text.js +40 -0
  896. package/dist/schema/Text.js.map +1 -0
  897. package/dist/schema/UnorderedList.d.ts +36 -0
  898. package/dist/schema/UnorderedList.d.ts.map +1 -0
  899. package/dist/schema/UnorderedList.js +42 -0
  900. package/dist/schema/UnorderedList.js.map +1 -0
  901. package/dist/schema/View.d.ts +81 -0
  902. package/dist/schema/View.d.ts.map +1 -0
  903. package/dist/schema/View.js +81 -0
  904. package/dist/schema/View.js.map +1 -0
  905. package/dist/schema/Wizard.d.ts +67 -0
  906. package/dist/schema/Wizard.d.ts.map +1 -0
  907. package/dist/schema/Wizard.js +94 -0
  908. package/dist/schema/Wizard.js.map +1 -0
  909. package/dist/schema/index.d.ts +26 -0
  910. package/dist/schema/index.d.ts.map +1 -0
  911. package/dist/schema/index.js +26 -0
  912. package/dist/schema/index.js.map +1 -0
  913. package/dist/schema/resolveSchema.d.ts +122 -0
  914. package/dist/schema/resolveSchema.d.ts.map +1 -0
  915. package/dist/schema/resolveSchema.js +648 -0
  916. package/dist/schema/resolveSchema.js.map +1 -0
  917. package/dist/schema/sanitize.d.ts +21 -0
  918. package/dist/schema/sanitize.d.ts.map +1 -0
  919. package/dist/schema/sanitize.js +46 -0
  920. package/dist/schema/sanitize.js.map +1 -0
  921. package/dist/search.d.ts +53 -0
  922. package/dist/search.d.ts.map +1 -0
  923. package/dist/search.js +114 -0
  924. package/dist/search.js.map +1 -0
  925. package/dist/sessionFilters.d.ts +8 -0
  926. package/dist/sessionFilters.d.ts.map +1 -0
  927. package/dist/sessionFilters.js +115 -0
  928. package/dist/sessionFilters.js.map +1 -0
  929. package/dist/summarizers/Summarizer.d.ts +65 -0
  930. package/dist/summarizers/Summarizer.d.ts.map +1 -0
  931. package/dist/summarizers/Summarizer.js +98 -0
  932. package/dist/summarizers/Summarizer.js.map +1 -0
  933. package/dist/summarizers/index.d.ts +2 -0
  934. package/dist/summarizers/index.d.ts.map +1 -0
  935. package/dist/summarizers/index.js +2 -0
  936. package/dist/summarizers/index.js.map +1 -0
  937. package/dist/theme/base-colors.d.ts +3 -0
  938. package/dist/theme/base-colors.d.ts.map +1 -0
  939. package/dist/theme/base-colors.js +64 -0
  940. package/dist/theme/base-colors.js.map +1 -0
  941. package/dist/theme/chart-colors.d.ts +3 -0
  942. package/dist/theme/chart-colors.d.ts.map +1 -0
  943. package/dist/theme/chart-colors.js +46 -0
  944. package/dist/theme/chart-colors.js.map +1 -0
  945. package/dist/theme/colors.d.ts +56 -0
  946. package/dist/theme/colors.d.ts.map +1 -0
  947. package/dist/theme/colors.js +410 -0
  948. package/dist/theme/colors.js.map +1 -0
  949. package/dist/theme/generate-css.d.ts +9 -0
  950. package/dist/theme/generate-css.d.ts.map +1 -0
  951. package/dist/theme/generate-css.js +36 -0
  952. package/dist/theme/generate-css.js.map +1 -0
  953. package/dist/theme/generate-scale.d.ts +3 -0
  954. package/dist/theme/generate-scale.d.ts.map +1 -0
  955. package/dist/theme/generate-scale.js +89 -0
  956. package/dist/theme/generate-scale.js.map +1 -0
  957. package/dist/theme/icon-map.d.ts +9 -0
  958. package/dist/theme/icon-map.d.ts.map +1 -0
  959. package/dist/theme/icon-map.js +40 -0
  960. package/dist/theme/icon-map.js.map +1 -0
  961. package/dist/theme/index.d.ts +15 -0
  962. package/dist/theme/index.d.ts.map +1 -0
  963. package/dist/theme/index.js +13 -0
  964. package/dist/theme/index.js.map +1 -0
  965. package/dist/theme/migrate.d.ts +14 -0
  966. package/dist/theme/migrate.d.ts.map +1 -0
  967. package/dist/theme/migrate.js +79 -0
  968. package/dist/theme/migrate.js.map +1 -0
  969. package/dist/theme/presets.d.ts +30 -0
  970. package/dist/theme/presets.d.ts.map +1 -0
  971. package/dist/theme/presets.js +128 -0
  972. package/dist/theme/presets.js.map +1 -0
  973. package/dist/theme/radius.d.ts +11 -0
  974. package/dist/theme/radius.d.ts.map +1 -0
  975. package/dist/theme/radius.js +17 -0
  976. package/dist/theme/radius.js.map +1 -0
  977. package/dist/theme/resolve.d.ts +13 -0
  978. package/dist/theme/resolve.d.ts.map +1 -0
  979. package/dist/theme/resolve.js +91 -0
  980. package/dist/theme/resolve.js.map +1 -0
  981. package/dist/theme/spacing.d.ts +14 -0
  982. package/dist/theme/spacing.d.ts.map +1 -0
  983. package/dist/theme/spacing.js +17 -0
  984. package/dist/theme/spacing.js.map +1 -0
  985. package/dist/theme/theme-colors.d.ts +9 -0
  986. package/dist/theme/theme-colors.d.ts.map +1 -0
  987. package/dist/theme/theme-colors.js +84 -0
  988. package/dist/theme/theme-colors.js.map +1 -0
  989. package/dist/theme/types.d.ts +94 -0
  990. package/dist/theme/types.d.ts.map +1 -0
  991. package/dist/theme/types.js +2 -0
  992. package/dist/theme/types.js.map +1 -0
  993. package/dist/uploads/UploadAdapter.d.ts +34 -0
  994. package/dist/uploads/UploadAdapter.d.ts.map +1 -0
  995. package/dist/uploads/UploadAdapter.js +2 -0
  996. package/dist/uploads/UploadAdapter.js.map +1 -0
  997. package/dist/uploads/index.d.ts +3 -0
  998. package/dist/uploads/index.d.ts.map +1 -0
  999. package/dist/uploads/index.js +2 -0
  1000. package/dist/uploads/index.js.map +1 -0
  1001. package/dist/uploads/localUpload.d.ts +25 -0
  1002. package/dist/uploads/localUpload.d.ts.map +1 -0
  1003. package/dist/uploads/localUpload.js +65 -0
  1004. package/dist/uploads/localUpload.js.map +1 -0
  1005. package/dist/validation/Validator.d.ts +40 -0
  1006. package/dist/validation/Validator.d.ts.map +1 -0
  1007. package/dist/validation/Validator.js +25 -0
  1008. package/dist/validation/Validator.js.map +1 -0
  1009. package/dist/validation/index.d.ts +5 -0
  1010. package/dist/validation/index.d.ts.map +1 -0
  1011. package/dist/validation/index.js +5 -0
  1012. package/dist/validation/index.js.map +1 -0
  1013. package/dist/validation/rules.d.ts +9 -0
  1014. package/dist/validation/rules.d.ts.map +1 -0
  1015. package/dist/validation/rules.js +61 -0
  1016. package/dist/validation/rules.js.map +1 -0
  1017. package/dist/validation/runValidators.d.ts +30 -0
  1018. package/dist/validation/runValidators.d.ts.map +1 -0
  1019. package/dist/validation/runValidators.js +438 -0
  1020. package/dist/validation/runValidators.js.map +1 -0
  1021. package/dist/validation/uniqueValidator.d.ts +61 -0
  1022. package/dist/validation/uniqueValidator.d.ts.map +1 -0
  1023. package/dist/validation/uniqueValidator.js +80 -0
  1024. package/dist/validation/uniqueValidator.js.map +1 -0
  1025. package/dist/vite.d.ts +19 -0
  1026. package/dist/vite.d.ts.map +1 -0
  1027. package/dist/vite.js +696 -0
  1028. package/dist/vite.js.map +1 -0
  1029. package/dist/widgets/index.d.ts +2 -0
  1030. package/dist/widgets/index.d.ts.map +1 -0
  1031. package/dist/widgets/index.js +7 -0
  1032. package/dist/widgets/index.js.map +1 -0
  1033. package/dist/widgets/registry.d.ts +32 -0
  1034. package/dist/widgets/registry.d.ts.map +1 -0
  1035. package/dist/widgets/registry.js +17 -0
  1036. package/dist/widgets/registry.js.map +1 -0
  1037. package/package.json +101 -0
  1038. package/src/Cluster.test.ts +283 -0
  1039. package/src/Cluster.ts +83 -0
  1040. package/src/Column.test.ts +140 -0
  1041. package/src/Column.ts +612 -0
  1042. package/src/Global.test.ts +367 -0
  1043. package/src/Global.ts +169 -0
  1044. package/src/Page.test.ts +50 -0
  1045. package/src/Page.ts +139 -0
  1046. package/src/Pilotiq.test.ts +47 -0
  1047. package/src/Pilotiq.ts +705 -0
  1048. package/src/PilotiqRegistry.ts +36 -0
  1049. package/src/PilotiqServiceProvider.ts +69 -0
  1050. package/src/RelationManager.test.ts +400 -0
  1051. package/src/RelationManager.ts +527 -0
  1052. package/src/RenderHook.test.ts +252 -0
  1053. package/src/RenderHook.ts +226 -0
  1054. package/src/Resource.test.ts +240 -0
  1055. package/src/Resource.ts +439 -0
  1056. package/src/RightPanel.test.ts +202 -0
  1057. package/src/RightPanel.ts +132 -0
  1058. package/src/Tab.test.ts +91 -0
  1059. package/src/Tab.ts +156 -0
  1060. package/src/UserMenuItem.ts +145 -0
  1061. package/src/actions/Action.test.ts +2479 -0
  1062. package/src/actions/Action.ts +2124 -0
  1063. package/src/actions/ActionGroup.test.ts +112 -0
  1064. package/src/actions/ActionGroup.ts +173 -0
  1065. package/src/actions/attachFactory.ts +172 -0
  1066. package/src/actions/exportFactory.ts +215 -0
  1067. package/src/actions/importFactory.ts +222 -0
  1068. package/src/actions/index.ts +17 -0
  1069. package/src/applyPageHooks.test.ts +298 -0
  1070. package/src/applyPageHooks.ts +242 -0
  1071. package/src/authorization.test.ts +483 -0
  1072. package/src/breadcrumbs.test.ts +238 -0
  1073. package/src/cells/coerce.test.ts +85 -0
  1074. package/src/cells/coerce.ts +84 -0
  1075. package/src/clusterPaths.ts +35 -0
  1076. package/src/columns/BadgeColumn.test.ts +54 -0
  1077. package/src/columns/BadgeColumn.ts +32 -0
  1078. package/src/columns/BooleanColumn.test.ts +41 -0
  1079. package/src/columns/BooleanColumn.ts +18 -0
  1080. package/src/columns/ColorColumn.test.ts +37 -0
  1081. package/src/columns/ColorColumn.ts +38 -0
  1082. package/src/columns/IconColumn.test.ts +54 -0
  1083. package/src/columns/IconColumn.ts +37 -0
  1084. package/src/columns/ImageColumn.test.ts +41 -0
  1085. package/src/columns/ImageColumn.ts +28 -0
  1086. package/src/columns/SelectColumn.ts +60 -0
  1087. package/src/columns/TextColumn.test.ts +190 -0
  1088. package/src/columns/TextColumn.ts +20 -0
  1089. package/src/columns/TextInputColumn.ts +68 -0
  1090. package/src/columns/ToggleColumn.ts +46 -0
  1091. package/src/columns/editableColumns.test.ts +193 -0
  1092. package/src/columns/index.ts +9 -0
  1093. package/src/defaultGlobalPages.ts +95 -0
  1094. package/src/defaultPages.test.ts +634 -0
  1095. package/src/defaultPages.ts +614 -0
  1096. package/src/defaultViewPage.test.ts +147 -0
  1097. package/src/elements/Form.test.ts +223 -0
  1098. package/src/elements/Form.ts +397 -0
  1099. package/src/elements/ListTabs.ts +28 -0
  1100. package/src/elements/Table.test.ts +422 -0
  1101. package/src/elements/Table.ts +816 -0
  1102. package/src/elements/TableGroup.test.ts +149 -0
  1103. package/src/elements/TableGroup.ts +199 -0
  1104. package/src/elements/dispatchAction.test.ts +463 -0
  1105. package/src/elements/dispatchAction.ts +355 -0
  1106. package/src/elements/dispatchForm.test.ts +455 -0
  1107. package/src/elements/dispatchForm.ts +1855 -0
  1108. package/src/elements/dispatchTable.test.ts +1247 -0
  1109. package/src/elements/dispatchTable.ts +666 -0
  1110. package/src/elements/index.ts +21 -0
  1111. package/src/entries/BadgeEntry.ts +39 -0
  1112. package/src/entries/CodeEntry.test.ts +40 -0
  1113. package/src/entries/CodeEntry.ts +52 -0
  1114. package/src/entries/ColorEntry.ts +63 -0
  1115. package/src/entries/ComponentEntry.test.ts +173 -0
  1116. package/src/entries/ComponentEntry.ts +95 -0
  1117. package/src/entries/Entry.ts +304 -0
  1118. package/src/entries/IconEntry.ts +49 -0
  1119. package/src/entries/ImageEntry.ts +61 -0
  1120. package/src/entries/KeyValueEntry.ts +47 -0
  1121. package/src/entries/RepeatableEntry.test.ts +239 -0
  1122. package/src/entries/RepeatableEntry.ts +173 -0
  1123. package/src/entries/TextEntry.test.ts +394 -0
  1124. package/src/entries/TextEntry.ts +60 -0
  1125. package/src/entries/index.ts +12 -0
  1126. package/src/entries/leaves.test.ts +306 -0
  1127. package/src/entries/registry.ts +54 -0
  1128. package/src/fields/BuilderField.test.ts +1188 -0
  1129. package/src/fields/BuilderField.ts +568 -0
  1130. package/src/fields/BuilderRelationship.test.ts +811 -0
  1131. package/src/fields/CheckboxField.test.ts +44 -0
  1132. package/src/fields/CheckboxField.ts +27 -0
  1133. package/src/fields/CheckboxListField.test.ts +99 -0
  1134. package/src/fields/CheckboxListField.ts +66 -0
  1135. package/src/fields/ColorPickerField.test.ts +33 -0
  1136. package/src/fields/ColorPickerField.ts +25 -0
  1137. package/src/fields/DateField.ts +54 -0
  1138. package/src/fields/DateTimeField.test.ts +55 -0
  1139. package/src/fields/EmailField.ts +16 -0
  1140. package/src/fields/Field.test.ts +639 -0
  1141. package/src/fields/Field.ts +773 -0
  1142. package/src/fields/FileUploadField.test.ts +97 -0
  1143. package/src/fields/FileUploadField.ts +71 -0
  1144. package/src/fields/HiddenField.test.ts +27 -0
  1145. package/src/fields/HiddenField.ts +28 -0
  1146. package/src/fields/KeyValueField.test.ts +105 -0
  1147. package/src/fields/KeyValueField.ts +55 -0
  1148. package/src/fields/MarkdownField.test.ts +167 -0
  1149. package/src/fields/MarkdownField.ts +151 -0
  1150. package/src/fields/NumberField.ts +33 -0
  1151. package/src/fields/RadioField.test.ts +94 -0
  1152. package/src/fields/RadioField.ts +67 -0
  1153. package/src/fields/RepeaterField.test.ts +1806 -0
  1154. package/src/fields/RepeaterField.ts +791 -0
  1155. package/src/fields/RepeaterRelationship.test.ts +1630 -0
  1156. package/src/fields/RepeaterSimple.test.ts +248 -0
  1157. package/src/fields/RowButton.test.ts +149 -0
  1158. package/src/fields/RowButton.ts +125 -0
  1159. package/src/fields/SelectField.test.ts +192 -0
  1160. package/src/fields/SelectField.ts +235 -0
  1161. package/src/fields/SliderField.test.ts +50 -0
  1162. package/src/fields/SliderField.ts +53 -0
  1163. package/src/fields/SlugField.ts +24 -0
  1164. package/src/fields/TagsInputField.test.ts +154 -0
  1165. package/src/fields/TagsInputField.ts +133 -0
  1166. package/src/fields/TextField.ts +24 -0
  1167. package/src/fields/TextareaField.test.ts +58 -0
  1168. package/src/fields/TextareaField.ts +59 -0
  1169. package/src/fields/ToggleButtonsField.test.ts +106 -0
  1170. package/src/fields/ToggleButtonsField.ts +59 -0
  1171. package/src/fields/ToggleField.ts +16 -0
  1172. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +319 -0
  1173. package/src/fields/optionsResolver.ts +95 -0
  1174. package/src/fields/resolveField.ts +28 -0
  1175. package/src/filters/BooleanFilter.ts +35 -0
  1176. package/src/filters/DateRangeFilter.test.ts +194 -0
  1177. package/src/filters/DateRangeFilter.ts +148 -0
  1178. package/src/filters/Filter.test.ts +268 -0
  1179. package/src/filters/Filter.ts +184 -0
  1180. package/src/filters/FormFilter.test.ts +238 -0
  1181. package/src/filters/FormFilter.ts +215 -0
  1182. package/src/filters/MultiSelectFilter.test.ts +119 -0
  1183. package/src/filters/MultiSelectFilter.ts +78 -0
  1184. package/src/filters/QueryBuilderFilter.test.ts +644 -0
  1185. package/src/filters/QueryBuilderFilter.ts +398 -0
  1186. package/src/filters/SelectFilter.ts +46 -0
  1187. package/src/filters/TernaryFilter.test.ts +160 -0
  1188. package/src/filters/TernaryFilter.ts +72 -0
  1189. package/src/filters/TrashedFilter.test.ts +149 -0
  1190. package/src/filters/TrashedFilter.ts +55 -0
  1191. package/src/filters/queryBuilder/BooleanConstraint.ts +31 -0
  1192. package/src/filters/queryBuilder/Constraint.ts +115 -0
  1193. package/src/filters/queryBuilder/DateConstraint.ts +69 -0
  1194. package/src/filters/queryBuilder/NumberConstraint.ts +66 -0
  1195. package/src/filters/queryBuilder/SelectConstraint.ts +72 -0
  1196. package/src/filters/queryBuilder/TextConstraint.ts +65 -0
  1197. package/src/filters/queryBuilder/index.ts +12 -0
  1198. package/src/icons/index.ts +2 -0
  1199. package/src/icons/lucide.ts +204 -0
  1200. package/src/icons/registry.test.ts +56 -0
  1201. package/src/icons/registry.ts +41 -0
  1202. package/src/icons/types.ts +47 -0
  1203. package/src/index.ts +521 -0
  1204. package/src/io/csv.test.ts +142 -0
  1205. package/src/io/csv.ts +170 -0
  1206. package/src/nestedRelationManagerData.test.ts +526 -0
  1207. package/src/notifications/Notification.test.ts +210 -0
  1208. package/src/notifications/Notification.ts +354 -0
  1209. package/src/notifications/broadcast.test.ts +110 -0
  1210. package/src/notifications/broadcast.ts +95 -0
  1211. package/src/notifications/database.test.ts +383 -0
  1212. package/src/notifications/database.ts +398 -0
  1213. package/src/notifications/databaseNotifications.test.ts +187 -0
  1214. package/src/notifications/dispatchNotificationAction.test.ts +341 -0
  1215. package/src/notifications/dispatchNotificationAction.ts +142 -0
  1216. package/src/notifications/flash.test.ts +89 -0
  1217. package/src/notifications/flash.ts +71 -0
  1218. package/src/notifications/index.ts +45 -0
  1219. package/src/notifications/registerBroadcastAuth.test.ts +134 -0
  1220. package/src/notifications/registerBroadcastAuth.ts +100 -0
  1221. package/src/notifications/resolveSavedNotification.test.ts +82 -0
  1222. package/src/notifications/resolveSavedNotification.ts +59 -0
  1223. package/src/notifications/types.ts +93 -0
  1224. package/src/orm/m2mAccessor.ts +66 -0
  1225. package/src/orm/modelDefaults.test.ts +633 -0
  1226. package/src/orm/modelDefaults.ts +632 -0
  1227. package/src/pageData.test.ts +1121 -0
  1228. package/src/pageData.ts +4662 -0
  1229. package/src/plugins/index.ts +1 -0
  1230. package/src/plugins/themeEditor.ts +24 -0
  1231. package/src/react/AppShell.tsx +148 -0
  1232. package/src/react/CommandPalette.tsx +375 -0
  1233. package/src/react/FormStateContext.tsx +398 -0
  1234. package/src/react/HeadHooks.tsx +126 -0
  1235. package/src/react/NotificationActionStrip.tsx +263 -0
  1236. package/src/react/NotificationBell.tsx +426 -0
  1237. package/src/react/RenderHookSlot.tsx +32 -0
  1238. package/src/react/RightSidebar.tsx +257 -0
  1239. package/src/react/RightSidebarContext.tsx +211 -0
  1240. package/src/react/RightSidebarTrigger.tsx +53 -0
  1241. package/src/react/SchemaRenderer.tsx +6128 -0
  1242. package/src/react/SearchTrigger.tsx +46 -0
  1243. package/src/react/ThemeProvider.tsx +93 -0
  1244. package/src/react/ThemeSettingsPage.tsx +579 -0
  1245. package/src/react/ThemeToggle.tsx +20 -0
  1246. package/src/react/Toaster.tsx +158 -0
  1247. package/src/react/UserMenu.tsx +196 -0
  1248. package/src/react/WidgetDataContext.tsx +157 -0
  1249. package/src/react/cells/EditableCell.tsx +376 -0
  1250. package/src/react/fieldJsHandler.test.ts +166 -0
  1251. package/src/react/fieldJsHandler.ts +79 -0
  1252. package/src/react/fields/BuilderInput.tsx +995 -0
  1253. package/src/react/fields/CheckboxInput.tsx +39 -0
  1254. package/src/react/fields/CheckboxListInput.tsx +81 -0
  1255. package/src/react/fields/ColorInput.tsx +51 -0
  1256. package/src/react/fields/DateFieldInput.tsx +70 -0
  1257. package/src/react/fields/DateTimeInput.tsx +42 -0
  1258. package/src/react/fields/FieldShell.tsx +107 -0
  1259. package/src/react/fields/FileUploadInput.tsx +189 -0
  1260. package/src/react/fields/HiddenInput.tsx +17 -0
  1261. package/src/react/fields/KeyValueInput.tsx +200 -0
  1262. package/src/react/fields/MarkdownInput.tsx +333 -0
  1263. package/src/react/fields/RadioInput.tsx +60 -0
  1264. package/src/react/fields/RepeaterInput.test.ts +116 -0
  1265. package/src/react/fields/RepeaterInput.tsx +1313 -0
  1266. package/src/react/fields/SelectFieldInput.tsx +257 -0
  1267. package/src/react/fields/SliderInput.tsx +63 -0
  1268. package/src/react/fields/TagsInput.tsx +265 -0
  1269. package/src/react/fields/TextLikeInput.tsx +54 -0
  1270. package/src/react/fields/ToggleButtonsInput.tsx +60 -0
  1271. package/src/react/fields/ToggleFieldInput.tsx +35 -0
  1272. package/src/react/fields/rowChromeButton.tsx +225 -0
  1273. package/src/react/fields/syncRowGates.test.ts +202 -0
  1274. package/src/react/fields/syncRowGates.ts +66 -0
  1275. package/src/react/formStateHelpers.test.ts +295 -0
  1276. package/src/react/formStateHelpers.ts +218 -0
  1277. package/src/react/hooks/use-mobile.ts +19 -0
  1278. package/src/react/icon-context.tsx +60 -0
  1279. package/src/react/index.ts +85 -0
  1280. package/src/react/layouts/SidebarLayout.tsx +239 -0
  1281. package/src/react/layouts/TopbarLayout.tsx +245 -0
  1282. package/src/react/navigate.tsx +37 -0
  1283. package/src/react/registry.ts +48 -0
  1284. package/src/react/right-panel-registry.tsx +47 -0
  1285. package/src/react/theme-preview/apply.ts +99 -0
  1286. package/src/react/theme-preview/build-html.ts +436 -0
  1287. package/src/react/ui/button.tsx +51 -0
  1288. package/src/react/ui/calendar.tsx +67 -0
  1289. package/src/react/ui/checkbox.tsx +29 -0
  1290. package/src/react/ui/dialog.tsx +108 -0
  1291. package/src/react/ui/dropdown-menu.tsx +97 -0
  1292. package/src/react/ui/input.tsx +20 -0
  1293. package/src/react/ui/label.tsx +21 -0
  1294. package/src/react/ui/popover.tsx +50 -0
  1295. package/src/react/ui/select.tsx +169 -0
  1296. package/src/react/ui/separator.tsx +25 -0
  1297. package/src/react/ui/sheet.tsx +136 -0
  1298. package/src/react/ui/sidebar.tsx +723 -0
  1299. package/src/react/ui/skeleton.tsx +13 -0
  1300. package/src/react/ui/slider.tsx +34 -0
  1301. package/src/react/ui/switch.tsx +28 -0
  1302. package/src/react/ui/table.tsx +105 -0
  1303. package/src/react/ui/tabs.tsx +63 -0
  1304. package/src/react/ui/textarea.tsx +18 -0
  1305. package/src/react/ui/tooltip.tsx +64 -0
  1306. package/src/react/useResizableWidth.ts +139 -0
  1307. package/src/react/utils.ts +6 -0
  1308. package/src/react/widgetRegistry.test.ts +43 -0
  1309. package/src/react/widgetRegistry.ts +50 -0
  1310. package/src/react/widgets/StatsOverviewRenderer.tsx +232 -0
  1311. package/src/react/widgets/TableWidgetRenderer.tsx +231 -0
  1312. package/src/react/widgets/ViewRenderer.tsx +71 -0
  1313. package/src/relationManagerData.test.ts +1146 -0
  1314. package/src/richtext/index.ts +8 -0
  1315. package/src/richtext/registry.ts +89 -0
  1316. package/src/routes-nested-relations.test.ts +676 -0
  1317. package/src/routes-relations.test.ts +972 -0
  1318. package/src/routes.test.ts +1886 -0
  1319. package/src/routes.ts +3262 -0
  1320. package/src/schema/Alert.test.ts +63 -0
  1321. package/src/schema/Alert.ts +49 -0
  1322. package/src/schema/Block.ts +169 -0
  1323. package/src/schema/Breadcrumbs.ts +40 -0
  1324. package/src/schema/Card.ts +35 -0
  1325. package/src/schema/Divider.ts +20 -0
  1326. package/src/schema/Element.ts +219 -0
  1327. package/src/schema/EmptyState.test.ts +37 -0
  1328. package/src/schema/EmptyState.ts +63 -0
  1329. package/src/schema/Fieldset.ts +43 -0
  1330. package/src/schema/Grid.ts +43 -0
  1331. package/src/schema/Group.ts +30 -0
  1332. package/src/schema/Heading.ts +39 -0
  1333. package/src/schema/Html.ts +67 -0
  1334. package/src/schema/Icon.ts +54 -0
  1335. package/src/schema/Image.ts +57 -0
  1336. package/src/schema/LinkTag.ts +41 -0
  1337. package/src/schema/Markdown.ts +85 -0
  1338. package/src/schema/MetaTag.ts +41 -0
  1339. package/src/schema/RelationTabs.ts +71 -0
  1340. package/src/schema/ScriptTag.ts +55 -0
  1341. package/src/schema/Section.ts +143 -0
  1342. package/src/schema/ServerDataElement.test.ts +140 -0
  1343. package/src/schema/ServerDataElement.ts +156 -0
  1344. package/src/schema/Split.ts +50 -0
  1345. package/src/schema/Stat.test.ts +118 -0
  1346. package/src/schema/Stat.ts +154 -0
  1347. package/src/schema/StatsOverview.test.ts +141 -0
  1348. package/src/schema/StatsOverview.ts +119 -0
  1349. package/src/schema/StyleTag.ts +35 -0
  1350. package/src/schema/TableWidget.test.ts +297 -0
  1351. package/src/schema/TableWidget.ts +289 -0
  1352. package/src/schema/Tabs.ts +79 -0
  1353. package/src/schema/Text.ts +58 -0
  1354. package/src/schema/UnorderedList.ts +49 -0
  1355. package/src/schema/View.test.ts +111 -0
  1356. package/src/schema/View.ts +127 -0
  1357. package/src/schema/Wizard.ts +108 -0
  1358. package/src/schema/containers.test.ts +446 -0
  1359. package/src/schema/headTags.test.ts +134 -0
  1360. package/src/schema/index.ts +39 -0
  1361. package/src/schema/primes.test.ts +269 -0
  1362. package/src/schema/resolveSchema.test.ts +329 -0
  1363. package/src/schema/resolveSchema.ts +807 -0
  1364. package/src/schema/sanitize.ts +49 -0
  1365. package/src/search.test.ts +446 -0
  1366. package/src/search.ts +178 -0
  1367. package/src/sessionFilters.test.ts +352 -0
  1368. package/src/sessionFilters.ts +133 -0
  1369. package/src/summarizers/Summarizer.test.ts +84 -0
  1370. package/src/summarizers/Summarizer.ts +123 -0
  1371. package/src/summarizers/index.ts +11 -0
  1372. package/src/theme/base-colors.ts +68 -0
  1373. package/src/theme/chart-colors.ts +50 -0
  1374. package/src/theme/colors.ts +447 -0
  1375. package/src/theme/generate-css.test.ts +139 -0
  1376. package/src/theme/generate-css.ts +44 -0
  1377. package/src/theme/generate-scale.test.ts +106 -0
  1378. package/src/theme/generate-scale.ts +97 -0
  1379. package/src/theme/icon-map.ts +42 -0
  1380. package/src/theme/index.ts +28 -0
  1381. package/src/theme/migrate.ts +81 -0
  1382. package/src/theme/presets.ts +135 -0
  1383. package/src/theme/radius.ts +18 -0
  1384. package/src/theme/resolve.test.ts +238 -0
  1385. package/src/theme/resolve.ts +96 -0
  1386. package/src/theme/spacing.ts +18 -0
  1387. package/src/theme/theme-colors.ts +88 -0
  1388. package/src/theme/types.ts +125 -0
  1389. package/src/uploads/UploadAdapter.ts +35 -0
  1390. package/src/uploads/index.ts +2 -0
  1391. package/src/uploads/localUpload.test.ts +70 -0
  1392. package/src/uploads/localUpload.ts +84 -0
  1393. package/src/validation/Validator.ts +49 -0
  1394. package/src/validation/index.ts +28 -0
  1395. package/src/validation/rules.ts +78 -0
  1396. package/src/validation/runValidators.ts +435 -0
  1397. package/src/validation/uniqueValidator.test.ts +196 -0
  1398. package/src/validation/uniqueValidator.ts +133 -0
  1399. package/src/validation/validators.test.ts +268 -0
  1400. package/src/vite.ts +758 -0
  1401. package/src/widgets/index.ts +10 -0
  1402. package/src/widgets/registry.ts +45 -0
  1403. package/src/widgets.test.ts +592 -0
  1404. package/tsconfig.build.json +11 -0
  1405. package/tsconfig.json +4 -0
  1406. package/tsconfig.test.json +10 -0
  1407. package/views/react/Dashboard.tsx +27 -0
  1408. package/views/react/Resources/Form.tsx +102 -0
  1409. package/views/react/Resources/Index.tsx +49 -0
@@ -0,0 +1,3725 @@
1
+ import { PilotiqRegistry } from './PilotiqRegistry.js';
2
+ import { resourceBasePath, globalBasePath, pageBasePath, clusterBasePath } from './clusterPaths.js';
3
+ import { Field } from './fields/Field.js';
4
+ import { resolveSchema } from './schema/resolveSchema.js';
5
+ import { isServerDataElement } from './schema/ServerDataElement.js';
6
+ import { Form } from './elements/Form.js';
7
+ import { Table } from './elements/Table.js';
8
+ import { Column } from './Column.js';
9
+ import { applyStateUpdate, coerceFormValues, findForms, findWizardStepFields, loadRelationRows, selectFormById } from './elements/dispatchForm.js';
10
+ import { isRepeaterField, RepeaterField } from './fields/RepeaterField.js';
11
+ import { isBuilderField, BuilderField } from './fields/BuilderField.js';
12
+ import { SelectField } from './fields/SelectField.js';
13
+ import { validateSchema } from './validation/index.js';
14
+ import { searchAllResources } from './search.js';
15
+ import { loadTableRecords, findTables } from './elements/dispatchTable.js';
16
+ import { findActions, findRowExtraActions } from './elements/dispatchAction.js';
17
+ import { Filter } from './filters/Filter.js';
18
+ import { TrashedFilter } from './filters/TrashedFilter.js';
19
+ import { resolveTheme } from './theme/resolve.js';
20
+ import { consumeFlashedNotifications } from './notifications/flash.js';
21
+ import { notificationChannel, NOTIFICATION_CREATED_EVENT, } from './notifications/broadcast.js';
22
+ import { serializeIcon } from './icons/types.js';
23
+ import { RIGHT_PANEL_DEFAULT_WIDTH, RIGHT_PANEL_MIN_WIDTH, RIGHT_PANEL_MAX_WIDTH, } from './RightPanel.js';
24
+ import { safeManagerPolicy as safeManagerPolicyImpl, } from './RelationManager.js';
25
+ import { RelationTabs, relationTab } from './schema/RelationTabs.js';
26
+ import { Breadcrumbs } from './schema/Breadcrumbs.js';
27
+ import { resolveRenderHooks, CHROME_HOOK_NAMES, } from './RenderHook.js';
28
+ import { applyPageHooks, pageHooksFor } from './applyPageHooks.js';
29
+ import { modelSave, modelLoadRecord, modelRelationTableRecords, findRecord, getPrimaryKey, getRelationType, getMorphRelationDescriptor, } from './orm/modelDefaults.js';
30
+ import { normalizeRelationMode } from './RelationManager.js';
31
+ export async function panelInfo(pilotiq, req, route = {}) {
32
+ const cfg = pilotiq.getConfig();
33
+ const merged = pilotiq.getMergedTheme();
34
+ const theme = merged ? resolveTheme(merged) : undefined;
35
+ const user = await pilotiq.resolveUser(req);
36
+ const [navigation, userMenu, renderHooks, rightSidebar] = await Promise.all([
37
+ buildNavigation(pilotiq, user),
38
+ buildUserMenu(pilotiq, user),
39
+ resolveChromeHooks(pilotiq, user, route),
40
+ buildRightSidebarMeta(cfg, user),
41
+ ]);
42
+ const databaseNotifications = buildDatabaseNotificationsMeta(cfg, user);
43
+ return {
44
+ name: cfg.name,
45
+ branding: cfg.branding,
46
+ navigation,
47
+ theme,
48
+ themeEditor: cfg.themeEditor ?? false,
49
+ ...(userMenu ? { userMenu } : {}),
50
+ ...(databaseNotifications ? { databaseNotifications } : {}),
51
+ ...(rightSidebar ? { rightSidebar } : {}),
52
+ ...(Object.keys(renderHooks).length > 0 ? { renderHooks } : {}),
53
+ };
54
+ }
55
+ /**
56
+ * Build the bell-icon meta. Returns `null` when:
57
+ * - `Pilotiq.databaseNotifications()` was never called, OR
58
+ * - no user resolves (no inbox to surface).
59
+ *
60
+ * Defaults follow Filament: 30s polling, 25 rows per page, primary
61
+ * badge color, topbar position.
62
+ */
63
+ function buildDatabaseNotificationsMeta(cfg, user) {
64
+ if (!cfg.databaseNotifications?.enabled)
65
+ return null;
66
+ if (user === null || user === undefined)
67
+ return null;
68
+ const dn = cfg.databaseNotifications;
69
+ const base = cfg.path;
70
+ const meta = {
71
+ position: dn.position ?? 'topbar',
72
+ polling: dn.polling === null ? null : (dn.polling ?? 30),
73
+ pageSize: dn.pageSize ?? 25,
74
+ badgeColor: dn.badgeColor ?? 'primary',
75
+ listUrl: `${base}/_notifications`,
76
+ readAllUrl: `${base}/_notifications/read-all`,
77
+ readUrl: `${base}/_notifications/:id/read`,
78
+ unreadUrl: `${base}/_notifications/:id/unread`,
79
+ actionUrl: `${base}/_notifications/:id/_action/:actionName`,
80
+ };
81
+ if (dn.trigger)
82
+ meta.trigger = { ...dn.trigger };
83
+ // Phase 2 broadcast hint — only ship when broadcast is enabled AND the
84
+ // resolved user has an `id` to scope the channel to. The client uses
85
+ // `wsUrl` for the WebSocket connection and `channel` for the subscribe
86
+ // call (the private- prefix is already baked in).
87
+ if (dn.broadcast) {
88
+ const userId = user?.id;
89
+ if (userId !== undefined && userId !== null) {
90
+ const wsUrl = typeof dn.broadcast === 'object' && dn.broadcast.wsUrl
91
+ ? dn.broadcast.wsUrl
92
+ : ''; // empty = client falls back to same-origin /ws
93
+ meta.broadcast = {
94
+ wsUrl,
95
+ channel: notificationChannel(String(userId)),
96
+ event: NOTIFICATION_CREATED_EVENT,
97
+ };
98
+ }
99
+ }
100
+ return meta;
101
+ }
102
+ /**
103
+ * Build the right-sidebar meta from registered contributions. Returns
104
+ * `null` when:
105
+ *
106
+ * - no contributions were registered, OR
107
+ * - every contribution failed `canAccess(user)` (or its predicate
108
+ * threw — fail-closed), OR
109
+ * - every passing contribution is `hidden: true` (no tab-strip
110
+ * surface to mount; programmatic-open consumers should ship at
111
+ * least one visible tab).
112
+ *
113
+ * Visible contributions are sorted by `sort` ascending (default 100),
114
+ * with registration order as a stable tiebreaker. Each entry's icon is
115
+ * serialized through `serializeIcon` keyed on the contribution `id`
116
+ * (Phase B's Vite plugin extends `_components.ts` to round-trip
117
+ * component-typed icons under that key). `defaultWidth` rolls up:
118
+ * panel-level baseline is `RIGHT_PANEL_DEFAULT_WIDTH`; per-contribution
119
+ * overrides ride on `RightPanelMeta.defaultWidth`.
120
+ *
121
+ * Errors thrown by `canAccess` are swallowed (the contribution is
122
+ * dropped + a single console warn is emitted) so a flaky predicate on
123
+ * one pane never blanks the whole sidebar.
124
+ */
125
+ async function buildRightSidebarMeta(cfg, user) {
126
+ const list = cfg.rightPanels ?? [];
127
+ if (list.length === 0)
128
+ return null;
129
+ const indexed = list.map((c, idx) => ({ c, idx }));
130
+ const gated = await Promise.all(indexed.map(async ({ c, idx }) => {
131
+ if (c.canAccess) {
132
+ try {
133
+ const ok = await c.canAccess(user);
134
+ if (!ok)
135
+ return null;
136
+ }
137
+ catch (err) {
138
+ // eslint-disable-next-line no-console
139
+ console.warn(`[Pilotiq] rightPanel "${c.id}" canAccess threw — dropping`, err);
140
+ return null;
141
+ }
142
+ }
143
+ return { c, idx };
144
+ }));
145
+ const visible = gated
146
+ .filter((x) => x !== null)
147
+ .filter((x) => !x.c.hidden)
148
+ .sort((a, b) => {
149
+ const sa = a.c.sort ?? 100;
150
+ const sb = b.c.sort ?? 100;
151
+ if (sa !== sb)
152
+ return sa - sb;
153
+ return a.idx - b.idx;
154
+ });
155
+ if (visible.length === 0)
156
+ return null;
157
+ const panels = visible.map(({ c }) => {
158
+ const meta = {
159
+ id: c.id,
160
+ label: c.label ?? c.id,
161
+ defaultWidth: c.defaultWidth ?? RIGHT_PANEL_DEFAULT_WIDTH,
162
+ };
163
+ if (c.icon !== undefined) {
164
+ meta.icon = serializeIcon(c.icon, c.id);
165
+ }
166
+ return meta;
167
+ });
168
+ return {
169
+ panels,
170
+ defaultWidth: panels[0]?.defaultWidth ?? RIGHT_PANEL_DEFAULT_WIDTH,
171
+ minWidth: RIGHT_PANEL_MIN_WIDTH,
172
+ maxWidth: RIGHT_PANEL_MAX_WIDTH,
173
+ };
174
+ }
175
+ /**
176
+ * Resolve every chrome render hook (body / topbar / sidebar / user-menu
177
+ * / footer / head). Returns a sparse map — slots with no matching
178
+ * registered entries are omitted so the wire payload stays minimal on
179
+ * panels that don't use render hooks at all.
180
+ */
181
+ async function resolveChromeHooks(pilotiq, user, route) {
182
+ const cfg = pilotiq.getConfig();
183
+ const entries = cfg.renderHooks ?? [];
184
+ if (entries.length === 0)
185
+ return {};
186
+ const ctx = {
187
+ user,
188
+ basePath: cfg.path,
189
+ url: route.url ?? cfg.path,
190
+ };
191
+ if (route.resource !== undefined)
192
+ ctx.resource = route.resource;
193
+ if (route.page !== undefined)
194
+ ctx.page = route.page;
195
+ if (route.global !== undefined)
196
+ ctx.global = route.global;
197
+ if (route.recordId !== undefined)
198
+ ctx.recordId = route.recordId;
199
+ return resolveRenderHooks(entries, CHROME_HOOK_NAMES, ctx);
200
+ }
201
+ /**
202
+ * Resolve a subset of page-role render hooks (e.g. `panels::page.start`
203
+ * + the list-records / create-record / view-record / edit-record /
204
+ * global-search slot families). Per-page-role data builders call this
205
+ * after schema resolution and stamp the result on `viewProps.renderHooks`.
206
+ *
207
+ * `names` lets each builder declare exactly which slots it serves so a
208
+ * list-page builder doesn't ship slots that only fire on the edit page.
209
+ */
210
+ /**
211
+ * Per-builder one-shot — resolve the role's slot set + splice the
212
+ * results into the resolved schema. Wraps the two steps a per-builder
213
+ * data fn always does in lockstep:
214
+ *
215
+ * 1. `resolvePageHooks(pilotiq, user, pageHooksFor(role), route)`
216
+ * 2. `applyPageHooks(schemaData, hooks, role)`
217
+ *
218
+ * Returns the wrapped `ElementMeta[]`. No-op when the panel has no
219
+ * registered hooks. Pass through what you'd pass to `panelInfo()`'s
220
+ * route arg — same shape.
221
+ */
222
+ export async function applyRoleHooks(pilotiq, user, role, schemaData, route = {}) {
223
+ const cfg = pilotiq.getConfig();
224
+ if (!cfg.renderHooks || cfg.renderHooks.length === 0)
225
+ return schemaData;
226
+ const hooks = await resolvePageHooks(pilotiq, user, pageHooksFor(role), route);
227
+ return applyPageHooks(schemaData, hooks, role);
228
+ }
229
+ export async function resolvePageHooks(pilotiq, user, names, route) {
230
+ const cfg = pilotiq.getConfig();
231
+ const entries = cfg.renderHooks ?? [];
232
+ if (entries.length === 0 || names.length === 0)
233
+ return {};
234
+ const ctx = {
235
+ user,
236
+ basePath: cfg.path,
237
+ url: route.url ?? cfg.path,
238
+ };
239
+ if (route.resource !== undefined)
240
+ ctx.resource = route.resource;
241
+ if (route.page !== undefined)
242
+ ctx.page = route.page;
243
+ if (route.global !== undefined)
244
+ ctx.global = route.global;
245
+ if (route.recordId !== undefined)
246
+ ctx.recordId = route.recordId;
247
+ return resolveRenderHooks(entries, names, ctx);
248
+ }
249
+ /**
250
+ * Build the top-right user-menu meta. Returns `null` when:
251
+ * - `Pilotiq.user()` isn't configured, or
252
+ * - the resolver returned `null` (anonymous request), or
253
+ * - the user object has no extractable identity AND the panel
254
+ * configured no items / no sign-out (nothing to render).
255
+ *
256
+ * Items resolve in parallel with their visibility predicates
257
+ * (`UserMenuItem.visible`). Throwing predicates fail closed (item
258
+ * dropped). Sort by `.sort(n)` ascending → registration order.
259
+ */
260
+ async function buildUserMenu(pilotiq, user) {
261
+ if (user === null || user === undefined)
262
+ return null;
263
+ const cfg = pilotiq.getConfig();
264
+ const items = cfg.userMenuItems ?? [];
265
+ const ctx = { user };
266
+ // Resolve every item in parallel. `null` returns mean "filtered by
267
+ // visibility predicate" — drop them. Indexed pre-sort so stable ties
268
+ // resolve to registration order.
269
+ const resolved = await Promise.all(items.map(async (item, idx) => {
270
+ try {
271
+ const meta = await item.resolve(ctx);
272
+ return meta ? { meta, idx, sort: item.getSort() } : null;
273
+ }
274
+ catch {
275
+ return null;
276
+ }
277
+ }));
278
+ const visibleItems = resolved
279
+ .filter((x) => x !== null)
280
+ .sort((a, b) => {
281
+ const aHas = a.sort !== undefined, bHas = b.sort !== undefined;
282
+ if (aHas && bHas)
283
+ return a.sort - b.sort || a.idx - b.idx;
284
+ if (aHas)
285
+ return -1;
286
+ if (bHas)
287
+ return 1;
288
+ return a.idx - b.idx;
289
+ })
290
+ .map(x => x.meta);
291
+ // Auto-inject the profile entry from `cfg.profilePage` when set.
292
+ // Prepended (Filament-style) so it always sits at the top of the
293
+ // dropdown regardless of user-authored item ordering. Falls through
294
+ // its own `canAccess(user)` so per-user gating works without the
295
+ // user repeating the predicate at the menu level.
296
+ const profileItem = await buildProfileMenuItem(cfg, user);
297
+ const finalItems = profileItem ? [profileItem, ...visibleItems] : visibleItems;
298
+ const meta = {
299
+ user: extractUserIdentity(user),
300
+ items: finalItems,
301
+ };
302
+ if (cfg.signOut) {
303
+ meta.signOut = {
304
+ url: cfg.signOut.url,
305
+ label: cfg.signOut.label ?? 'Sign out',
306
+ method: cfg.signOut.method ?? 'POST',
307
+ };
308
+ }
309
+ return meta;
310
+ }
311
+ /** Build the auto-injected profile entry from `cfg.profilePage`. The
312
+ * Page's `static label` / `static icon` win; defaults `'Edit profile'`
313
+ * + `'user-circle'` (registry-resolved). Returns `null` when no
314
+ * profile page is configured or `Page.canAccess(user)` denies. */
315
+ async function buildProfileMenuItem(cfg, user) {
316
+ const P = cfg.profilePage;
317
+ if (!P)
318
+ return null;
319
+ if (!(await safeAccess(() => P.canAccess(user))))
320
+ return null;
321
+ const url = pageBasePath(cfg.path, P);
322
+ const icon = serializeIcon(P.icon ?? 'user-circle', P.name);
323
+ const meta = {
324
+ name: '__profile',
325
+ label: P.label ?? 'Edit profile',
326
+ url,
327
+ };
328
+ if (icon !== undefined)
329
+ meta.icon = icon;
330
+ return meta;
331
+ }
332
+ /** Duck-type the user object for display fields. We never throw — a
333
+ * user resolver might return literally anything (a primitive, a class
334
+ * instance with getters, a plain object) and the dropdown should
335
+ * degrade gracefully (initials fallback to '?' when no name found). */
336
+ function extractUserIdentity(user) {
337
+ if (user === null || user === undefined)
338
+ return {};
339
+ if (typeof user !== 'object')
340
+ return { name: String(user) };
341
+ const obj = user;
342
+ const out = {};
343
+ const name = obj.name ?? obj.fullName ?? obj.displayName ?? obj.username;
344
+ if (typeof name === 'string' && name)
345
+ out.name = name;
346
+ if (typeof obj.email === 'string' && obj.email)
347
+ out.email = obj.email;
348
+ const avatar = obj.avatar ?? obj.avatarUrl ?? obj.image;
349
+ if (typeof avatar === 'string' && avatar)
350
+ out.avatar = avatar;
351
+ return out;
352
+ }
353
+ /** Run a `canAccess` check, swallowing throws as `false`. Used by
354
+ * `buildNavigation` to fail-closed on flaky auth predicates without
355
+ * blanking the page. */
356
+ async function safeAccess(fn) {
357
+ try {
358
+ return Boolean(await fn());
359
+ }
360
+ catch {
361
+ return false;
362
+ }
363
+ }
364
+ /** Plan #10 — stamp the resolved user onto a SchemaContext so action
365
+ * visibility predicates can see it during `resolveSchema`. The `user`
366
+ * field is opaque (whatever `Pilotiq.user(req => …)` returns); skipped
367
+ * when null/undefined to keep ctx tidy. */
368
+ function userCtx(ctx, user) {
369
+ if (user === null || user === undefined)
370
+ return ctx;
371
+ return { ...ctx, user: user };
372
+ }
373
+ /** Plan #6 — stamp the panel-wide upload URL so `FileUpload` fields
374
+ * emit it on their meta. Single URL for the whole panel; no per-field
375
+ * variation. The route is always registered (see `_uploads` in
376
+ * `routes.ts`) — meta is stamped regardless of whether an adapter is
377
+ * configured so the renderer can show a clear error rather than
378
+ * silently breaking. The companion `hasUploadAdapter` flag distinguishes
379
+ * "URL exists but adapter missing" so fields with optional upload
380
+ * affordances (e.g. `MarkdownField`'s `attachFiles` button) can hide
381
+ * themselves rather than render a broken control. */
382
+ function uploadCtx(ctx, cfg) {
383
+ return {
384
+ ...ctx,
385
+ uploadUrl: `${cfg.path}/_uploads`,
386
+ ...(cfg.uploads ? { hasUploadAdapter: true } : {}),
387
+ };
388
+ }
389
+ async function buildNavigation(pilotiq, user) {
390
+ const cfg = pilotiq.getConfig();
391
+ const base = cfg.path;
392
+ // Flatten + resolve badges in parallel. We build the raw list first so
393
+ // every entry has its identity (`name`) and parent set; badges resolve
394
+ // alongside.
395
+ const raw = [];
396
+ let idx = 0;
397
+ const pushBadge = [];
398
+ // Plan #10 — pre-evaluate canAccess for every owner in parallel so we
399
+ // can drop forbidden items before flattening. Failed predicates fail
400
+ // closed (treated as `false`) so a thrown auth check doesn't accidentally
401
+ // expose nav items. Clusters compose: a child gated through its
402
+ // cluster's `canAccess` returning false drops the child even when the
403
+ // child's own predicate would have passed.
404
+ const [resourceAccess, globalAccess, pageAccess, clusterAccess] = await Promise.all([
405
+ Promise.all(cfg.resources.map(R => safeAccess(() => R.canAccess(user)))),
406
+ Promise.all(cfg.globals.map(G => safeAccess(() => G.canAccess(user)))),
407
+ Promise.all(cfg.pages.map(P => safeAccess(() => P.canAccess(user)))),
408
+ Promise.all(cfg.clusters.map(C => safeAccess(() => C.canAccess(user)))),
409
+ ]);
410
+ // Identity-keyed so two clusters that happen to share a `.name`
411
+ // (minifier collisions, hot-reload duplicate imports) don't clobber.
412
+ const clusterAccessByClass = new Map();
413
+ cfg.clusters.forEach((C, i) => clusterAccessByClass.set(C, !!clusterAccess[i]));
414
+ const firstChildUrlByCluster = new Map();
415
+ const recordChildUrl = (cluster, url) => {
416
+ if (!firstChildUrlByCluster.has(cluster))
417
+ firstChildUrlByCluster.set(cluster, url);
418
+ };
419
+ for (let i = 0; i < cfg.resources.length; i++) {
420
+ const R = cfg.resources[i];
421
+ if (!resourceAccess[i])
422
+ continue;
423
+ if (R.cluster && !clusterAccessByClass.get(R.cluster))
424
+ continue;
425
+ const url = resourceBasePath(base, R);
426
+ if (R.cluster)
427
+ recordChildUrl(R.cluster, url);
428
+ const item = {
429
+ name: R.name,
430
+ label: R.getNavigationLabel(),
431
+ url,
432
+ icon: serializeIcon(R.getNavigationIcon(), R.name),
433
+ _idx: idx++,
434
+ };
435
+ if (R.navigationGroup !== undefined)
436
+ item.group = R.navigationGroup;
437
+ if (R.navigationSort !== undefined)
438
+ item.sort = R.navigationSort;
439
+ // Cluster nesting wins over `navigationParentItem`. Both being set
440
+ // is a misconfiguration; cluster placement is the structural one.
441
+ if (R.cluster)
442
+ item.parent = R.cluster.name;
443
+ else if (R.navigationParentItem !== undefined)
444
+ item.parent = R.navigationParentItem;
445
+ if (R.navigationBadgeColor !== 'default')
446
+ item.badgeColor = R.navigationBadgeColor;
447
+ if (R.navigationBadge)
448
+ pushBadge.push({ item, handler: R.navigationBadge });
449
+ raw.push(item);
450
+ }
451
+ for (let i = 0; i < cfg.globals.length; i++) {
452
+ if (!globalAccess[i])
453
+ continue;
454
+ const G = cfg.globals[i];
455
+ if (G.cluster && !clusterAccessByClass.get(G.cluster))
456
+ continue;
457
+ // Globals default `navigationGroup` to `'Settings'`. Allow `null` as
458
+ // an explicit opt-out → render at top level.
459
+ const group = G.navigationGroup === null ? undefined : G.navigationGroup;
460
+ const url = globalBasePath(base, G);
461
+ if (G.cluster)
462
+ recordChildUrl(G.cluster, url);
463
+ const item = {
464
+ name: G.name,
465
+ label: G.getNavigationLabel(),
466
+ url,
467
+ icon: serializeIcon(G.getNavigationIcon(), G.name),
468
+ _idx: idx++,
469
+ };
470
+ if (group !== undefined)
471
+ item.group = group;
472
+ if (G.navigationSort !== undefined)
473
+ item.sort = G.navigationSort;
474
+ if (G.cluster)
475
+ item.parent = G.cluster.name;
476
+ else if (G.navigationParentItem !== undefined)
477
+ item.parent = G.navigationParentItem;
478
+ if (G.navigationBadgeColor !== 'default')
479
+ item.badgeColor = G.navigationBadgeColor;
480
+ if (G.navigationBadge)
481
+ pushBadge.push({ item, handler: G.navigationBadge });
482
+ raw.push(item);
483
+ }
484
+ for (let i = 0; i < cfg.pages.length; i++) {
485
+ if (!pageAccess[i])
486
+ continue;
487
+ const P = cfg.pages[i];
488
+ if (P.cluster && !clusterAccessByClass.get(P.cluster))
489
+ continue;
490
+ // The dashboard page collapses its nav URL to `${base}` so the
491
+ // sidebar entry deep-links to the panel root rather than
492
+ // `${base}/${P.getSlug()}` (which would 404 — the slug route skips
493
+ // the dashboard page at boot).
494
+ const isDashboard = cfg.dashboardPage === P;
495
+ const url = isDashboard ? base : pageBasePath(base, P);
496
+ if (P.cluster && !isDashboard)
497
+ recordChildUrl(P.cluster, url);
498
+ const item = {
499
+ name: P.name,
500
+ label: P.getNavigationLabel(),
501
+ url,
502
+ icon: serializeIcon(P.getNavigationIcon(), P.name),
503
+ _idx: idx++,
504
+ };
505
+ if (P.navigationGroup !== undefined)
506
+ item.group = P.navigationGroup;
507
+ if (P.navigationSort !== undefined)
508
+ item.sort = P.navigationSort;
509
+ if (P.cluster && !isDashboard)
510
+ item.parent = P.cluster.name;
511
+ else if (P.navigationParentItem !== undefined)
512
+ item.parent = P.navigationParentItem;
513
+ if (P.navigationBadgeColor !== 'default')
514
+ item.badgeColor = P.navigationBadgeColor;
515
+ if (P.navigationBadge)
516
+ pushBadge.push({ item, handler: P.navigationBadge });
517
+ raw.push(item);
518
+ }
519
+ // Clusters render as first-class nav items. Each gets a URL pointing
520
+ // at its `landingPage` (when set + accessible) or its first accessible
521
+ // child. Clusters whose every child was gated out are dropped silently
522
+ // — same posture as `navigationParentItem` with no resolvable parent.
523
+ for (let i = 0; i < cfg.clusters.length; i++) {
524
+ if (!clusterAccess[i])
525
+ continue;
526
+ const C = cfg.clusters[i];
527
+ let url;
528
+ if (C.landingPage) {
529
+ const lpIdx = cfg.pages.indexOf(C.landingPage);
530
+ if (lpIdx !== -1 && pageAccess[lpIdx]) {
531
+ url = cfg.dashboardPage === C.landingPage ? base : pageBasePath(base, C.landingPage);
532
+ }
533
+ }
534
+ if (url === undefined)
535
+ url = firstChildUrlByCluster.get(C);
536
+ if (url === undefined)
537
+ continue; // empty cluster — drop entirely
538
+ const item = {
539
+ name: C.name,
540
+ label: C.getNavigationLabel(),
541
+ url,
542
+ icon: serializeIcon(C.getNavigationIcon(), C.name),
543
+ _idx: idx++,
544
+ };
545
+ if (C.navigationGroup !== undefined)
546
+ item.group = C.navigationGroup;
547
+ if (C.navigationSort !== undefined)
548
+ item.sort = C.navigationSort;
549
+ if (C.navigationParentItem !== undefined)
550
+ item.parent = C.navigationParentItem;
551
+ if (C.navigationBadgeColor !== 'default')
552
+ item.badgeColor = C.navigationBadgeColor;
553
+ if (C.navigationBadge)
554
+ pushBadge.push({ item, handler: C.navigationBadge });
555
+ raw.push(item);
556
+ }
557
+ await Promise.all(pushBadge.map(async ({ item, handler }) => {
558
+ try {
559
+ const v = await handler();
560
+ if (v === undefined || v === null)
561
+ return;
562
+ item.badge = String(v);
563
+ }
564
+ catch {
565
+ // Per-badge errors stay silent.
566
+ }
567
+ }));
568
+ return nestAndSort(raw);
569
+ }
570
+ /**
571
+ * Resolve `parent` references → nest, drop cycles, sort within each
572
+ * grouping, then strip internal scaffolding (`parent`, `_idx`).
573
+ */
574
+ function nestAndSort(raw) {
575
+ const byName = new Map();
576
+ for (const it of raw)
577
+ byName.set(it.name, it);
578
+ // Detect parent cycles: walk upwards from each item; any name seen
579
+ // twice → cycle. Items in a cycle get treated as top-level.
580
+ const inCycle = new Set();
581
+ for (const it of raw) {
582
+ if (it.parent === undefined)
583
+ continue;
584
+ const seen = new Set([it.name]);
585
+ let cur = it.parent;
586
+ while (cur !== undefined) {
587
+ if (seen.has(cur)) {
588
+ if (typeof console !== 'undefined' && typeof console.warn === 'function') {
589
+ console.warn(`[Pilotiq] navigationParentItem cycle detected at "${it.name}" — rendering at top level.`);
590
+ }
591
+ inCycle.add(it.name);
592
+ break;
593
+ }
594
+ seen.add(cur);
595
+ const parent = byName.get(cur);
596
+ if (!parent)
597
+ break;
598
+ cur = parent.parent;
599
+ }
600
+ }
601
+ const childrenOf = new Map();
602
+ const top = [];
603
+ for (const it of raw) {
604
+ const parent = it.parent;
605
+ if (parent && byName.has(parent) && !inCycle.has(it.name)) {
606
+ const list = childrenOf.get(parent) ?? [];
607
+ list.push(it);
608
+ childrenOf.set(parent, list);
609
+ }
610
+ else {
611
+ top.push(it);
612
+ }
613
+ }
614
+ // Sort items in a sibling group by sort (asc), ties → registration order.
615
+ const sortItems = (items) => {
616
+ return [...items].sort((a, b) => {
617
+ const aHas = a.sort !== undefined, bHas = b.sort !== undefined;
618
+ if (aHas && bHas)
619
+ return a.sort - b.sort || a._idx - b._idx;
620
+ if (aHas)
621
+ return -1; // sorted items come before unsorted
622
+ if (bHas)
623
+ return 1;
624
+ return a._idx - b._idx;
625
+ });
626
+ };
627
+ // Strip internals + recurse into children.
628
+ const finalize = (items) => sortItems(items).map(it => {
629
+ const kids = childrenOf.get(it.name);
630
+ const { parent, _idx, ...rest } = it;
631
+ const out = { ...rest };
632
+ if (kids && kids.length > 0)
633
+ out.children = finalize(kids);
634
+ return out;
635
+ });
636
+ return finalize(top);
637
+ }
638
+ export async function callPageSchema(PageClass, ctx) {
639
+ return Promise.resolve(PageClass.schema(ctx));
640
+ }
641
+ /** Mark every Form on the page with its action URL so the rendered <form> posts to itself. */
642
+ export function tagFormActions(elements, action) {
643
+ for (const form of findForms(elements)) {
644
+ if (!form.getAction())
645
+ form.action(action);
646
+ }
647
+ }
648
+ /**
649
+ * Plan #5 — stamp the partial-resolve endpoint URL on every form whose
650
+ * descendants include at least one `live()` field. The client uses
651
+ * `FormMeta.stateUrl` to flip into controlled-state mode; forms without
652
+ * any live fields stay uncontrolled (zero-cost legacy path).
653
+ *
654
+ * `urlBuilder(formId)` lets the caller compose a per-form URL — the
655
+ * endpoint shape is `${base}/${slug}/_form/${formId}/state` so each
656
+ * form on a multi-form page gets its own route segment.
657
+ */
658
+ export function tagFormStateUrls(elements, urlBuilder) {
659
+ for (const form of findForms(elements)) {
660
+ if (formHasLiveField(form)) {
661
+ form.withStateUrl(urlBuilder(form.getFormId()));
662
+ }
663
+ }
664
+ }
665
+ /**
666
+ * Reorderable rows — stamp the POST-reorder URL on every `Table` that
667
+ * has `Table.reorderable()` set. The renderer reads `TableMeta.reorderUrl`
668
+ * to wire the drop handler; tables that aren't reorderable skip wiring
669
+ * entirely. Same shape as `tagFormStateUrls` so the call site stays
670
+ * consistent.
671
+ */
672
+ export function tagTableReorderUrls(elements, url) {
673
+ for (const table of findTables(elements)) {
674
+ if (table.isReorderable() && !table.getReorderUrl()) {
675
+ table.withReorderUrl(url);
676
+ }
677
+ }
678
+ }
679
+ // Marks every Table on the page deferred and stamps the URL the
680
+ // renderer will fetch from after mount. Must run BEFORE `loadTableRecords`
681
+ // so the records handler short-circuits.
682
+ export function tagTableDeferred(elements, url) {
683
+ for (const table of findTables(elements)) {
684
+ table.withDeferred(true);
685
+ table.withTableUrl(url);
686
+ }
687
+ }
688
+ /**
689
+ * Editable cell columns — walk every table on the page and stamp
690
+ * `_cellEditUrls[colName]` per row, but only on rows that already
691
+ * carry a `_cellEditable[colName]` marker (set by `loadTableRecords`
692
+ * after `R.canEdit(user, row)` passed). The dispatcher stays
693
+ * URL-shape-agnostic; URL building lives here parallel to
694
+ * `tagFormStateUrls / tagTableReorderUrls`.
695
+ *
696
+ * `idOf` extracts the per-row primary key. Defaults to reading `id` —
697
+ * works for the rudder ORM convention. Resources with a different
698
+ * primary-key column should pass an override (none in v1).
699
+ */
700
+ export function tagCellEditUrls(elements, resourceUrl, idOf = row => row['id']) {
701
+ for (const table of findTables(elements)) {
702
+ const rows = table.getRows();
703
+ if (!rows || rows.length === 0)
704
+ continue;
705
+ // Optimisation: skip the table when none of its columns are editable.
706
+ const editable = (table.getChildren() ?? []).some(c => c instanceof Column && c.isEditable());
707
+ if (!editable)
708
+ continue;
709
+ for (const row of rows) {
710
+ const editableMap = row['_cellEditable'];
711
+ if (!editableMap)
712
+ continue;
713
+ const id = idOf(row);
714
+ if (id === undefined || id === null || id === '')
715
+ continue;
716
+ const urls = {};
717
+ for (const colName of Object.keys(editableMap)) {
718
+ urls[colName] = `${resourceUrl}/${encodeURIComponent(String(id))}/_cell/${encodeURIComponent(colName)}`;
719
+ }
720
+ ;
721
+ row['_cellEditUrls'] = urls;
722
+ }
723
+ }
724
+ }
725
+ /**
726
+ * Plan #8 — stamp the wizard step-validate endpoint URL on every form
727
+ * whose descendants include a `Wizard` element. `FormMeta.wizardUrl` is
728
+ * what the client posts to on Next-button clicks; forms without a wizard
729
+ * descendant skip wiring.
730
+ */
731
+ export function tagFormWizardUrls(elements, urlBuilder) {
732
+ for (const form of findForms(elements)) {
733
+ if (formHasWizard(form)) {
734
+ form.withWizardUrl(urlBuilder(form.getFormId()));
735
+ }
736
+ }
737
+ }
738
+ /**
739
+ * Audit row 2026-05-07 cont'd⁸ — stamp the inline-create-option endpoint
740
+ * URL on every `SelectField` that has called `createOptionForm()`. Walks
741
+ * every form on the page so the URL carries the parent form's id; URL
742
+ * shape `${formScopeUrl}/_form/${formId}/create-option/${fieldName}` so
743
+ * the route handler can pick the form by id and the field by name.
744
+ *
745
+ * Mirrors `tagFormStateUrls / tagFormWizardUrls` — operates on the
746
+ * un-resolved Element tree, mutates field-instance state via
747
+ * `field.withCreateOptionUrl(url)`, and the field's `toMeta()` reads it
748
+ * back to emit `createOption.url`.
749
+ *
750
+ * Stops at Repeater / Builder boundaries (parallel to the form-state /
751
+ * wizard walkers): inside-row schemas are dispatched per-row and the
752
+ * createOption shape doesn't compose with row body coercion in v1.
753
+ */
754
+ export function tagSelectCreateOptionUrls(elements, urlBuilder) {
755
+ for (const form of findForms(elements)) {
756
+ const formId = form.getFormId();
757
+ walkSelectFields(form.getChildren() ?? [], (field) => {
758
+ if (field.hasCreateOption() && !field.getCreateOptionUrl()) {
759
+ field.withCreateOptionUrl(urlBuilder(formId, field.name));
760
+ }
761
+ });
762
+ }
763
+ }
764
+ function walkSelectFields(elements, visit) {
765
+ for (const el of elements) {
766
+ if (el instanceof SelectField) {
767
+ visit(el);
768
+ // SelectField has no children of its own — no recursion needed.
769
+ continue;
770
+ }
771
+ // Stop at row-array boundaries — see comment on `tagSelectCreateOptionUrls`.
772
+ if (el instanceof RepeaterField)
773
+ continue;
774
+ if (el instanceof BuilderField)
775
+ continue;
776
+ const children = el.getChildren();
777
+ if (children && children.length > 0)
778
+ walkSelectFields(children, visit);
779
+ }
780
+ }
781
+ function isAsyncMentionField(el) {
782
+ if (el.getType() !== 'richtext')
783
+ return false;
784
+ const candidate = el;
785
+ return typeof candidate.hasAsyncMentions === 'function'
786
+ && typeof candidate.withMentionsUrl === 'function';
787
+ }
788
+ export function tagRichTextMentionUrls(elements, urlBuilder) {
789
+ for (const form of findForms(elements)) {
790
+ const url = urlBuilder(form.getFormId());
791
+ let stampedAny = false;
792
+ const visit = (els) => {
793
+ for (const el of els) {
794
+ // Don't cross into nested forms — each form gets its own URL.
795
+ if (el !== form && el.getType() === 'form')
796
+ continue;
797
+ if (isAsyncMentionField(el) && el.hasAsyncMentions()) {
798
+ el.withMentionsUrl(url);
799
+ stampedAny = true;
800
+ }
801
+ // Builder.getChildren() returns undefined to keep the field-level
802
+ // walkers from treating heterogeneous rows as flat children. Manual
803
+ // descent into each block's schema covers the URL-stamping path
804
+ // without changing the no-cross posture for save/coerce.
805
+ if (isBuilderField(el)) {
806
+ for (const block of el.getBlocks())
807
+ visit(block.getSchema());
808
+ continue;
809
+ }
810
+ const children = el.getChildren();
811
+ if (children)
812
+ visit(children);
813
+ }
814
+ };
815
+ const children = form.getChildren();
816
+ if (children)
817
+ visit(children);
818
+ void stampedAny; // silence unused — kept locally for readability
819
+ }
820
+ }
821
+ function formHasLiveField(form) {
822
+ let found = false;
823
+ const visit = (els) => {
824
+ for (const el of els) {
825
+ if (found)
826
+ return;
827
+ // Either a server-side `live()` (drives a roundtrip) OR a
828
+ // client-side `afterStateUpdatedJs(body)` (JS-only) is enough to
829
+ // mount the controlled-form path: the FormStateProvider holds the
830
+ // values map either path needs, and the client gates the actual
831
+ // network POST on `live` separately. Cost of the over-stamp for
832
+ // JS-only forms is one unused endpoint URL per form — endpoint
833
+ // never gets hit because the client only POSTs on `live`.
834
+ if (el instanceof Field && (el.isLive() || el.getAfterStateUpdatedJs() !== undefined)) {
835
+ found = true;
836
+ return;
837
+ }
838
+ const children = el.getChildren();
839
+ if (children)
840
+ visit(children);
841
+ }
842
+ };
843
+ const children = form.getChildren();
844
+ if (children)
845
+ visit(children);
846
+ return found;
847
+ }
848
+ function formHasWizard(form) {
849
+ let found = false;
850
+ const visit = (els) => {
851
+ for (const el of els) {
852
+ if (found)
853
+ return;
854
+ if (el.getType() === 'wizard') {
855
+ found = true;
856
+ return;
857
+ }
858
+ const children = el.getChildren();
859
+ if (children)
860
+ visit(children);
861
+ }
862
+ };
863
+ const children = form.getChildren();
864
+ if (children)
865
+ visit(children);
866
+ return found;
867
+ }
868
+ /**
869
+ * Run the edit-mode fill pipeline on a loaded record:
870
+ * mutateFormDataBeforeFill → fillFromRecord → mutateFormDataAfterFill
871
+ *
872
+ * `fillFromRecord` defaults to `{ ...record }` when not configured. Both
873
+ * mutators are optional and may be async. `ctx.record` is the loaded
874
+ * record so mutators can read from fields the form doesn't surface.
875
+ */
876
+ export async function applyFillPipeline(form, record) {
877
+ const recordObj = record;
878
+ let values = { ...recordObj };
879
+ const before = form.getMutateFormDataBeforeFill();
880
+ if (before)
881
+ values = await before(values, { values, record });
882
+ const fill = form.getFillFromRecord();
883
+ if (fill)
884
+ values = fill(record);
885
+ const after = form.getMutateFormDataAfterFill();
886
+ if (after)
887
+ values = await after(values, { values, record });
888
+ return values;
889
+ }
890
+ /**
891
+ * Walk the form's top-level Repeaters and replace `values[fieldName]`
892
+ * with rows fetched from `parent.related(name)` for any
893
+ * relationship-backed Repeater. Each loaded row stamps `__id` to the
894
+ * child's primary key so the renderer can round-trip identity through
895
+ * a hidden input and the save-side diff can match submitted rows back
896
+ * to existing records.
897
+ *
898
+ * No-op when the parent record is null (create mode), when no
899
+ * relationship-backed Repeaters exist on the form, or when the
900
+ * resource has no `R.model` (relation queries need it).
901
+ *
902
+ * Mutates and returns a fresh values object — never the input.
903
+ */
904
+ export async function applyRelationshipRepeaterFill(form, values, record, parentModel) {
905
+ if (record == null)
906
+ return values;
907
+ if (!parentModel)
908
+ return values;
909
+ const repeaters = findRelationshipRepeaters(form.getChildren() ?? []);
910
+ if (repeaters.length === 0)
911
+ return values;
912
+ const out = { ...values };
913
+ for (const repeater of repeaters) {
914
+ const cfg = repeater.getRelationship();
915
+ const pivotColumns = cfg.pivotColumns;
916
+ let rows;
917
+ try {
918
+ rows = await loadRelationRows(parentModel, record, cfg.name, pivotColumns);
919
+ }
920
+ catch {
921
+ // Failed lookup (e.g. missing `relations` map on a test stub)
922
+ // — fall back to whatever value applyFillPipeline produced
923
+ // rather than wiping the field. Better to render stale data
924
+ // than to silently empty the row list.
925
+ continue;
926
+ }
927
+ // The child model is opaque here — we don't have the full
928
+ // descriptor at this seam, so use the configured override or
929
+ // peek the parent's relations map for the FK column. Strip it
930
+ // (and the PK) from each row's payload so the inner schema
931
+ // doesn't surface them as form values. For morphMany the
932
+ // attachment is two columns instead of one — strip both.
933
+ const pkColumn = pickChildPrimaryKey(parentModel, cfg.name) ?? 'id';
934
+ const fkColumn = cfg.foreignKey ?? pickChildForeignKey(parentModel, cfg.name);
935
+ const morph = getMorphRelationDescriptor(parentModel, cfg.name);
936
+ const morphIdCol = morph ? `${morph.morphName}Id` : undefined;
937
+ const morphTyCol = morph ? `${morph.morphName}Type` : undefined;
938
+ out[repeater.name] = rows.map(row => {
939
+ const r = (row && typeof row === 'object') ? { ...row } : {};
940
+ const pkValue = r[pkColumn];
941
+ delete r[pkColumn];
942
+ if (fkColumn)
943
+ delete r[fkColumn];
944
+ if (morphIdCol)
945
+ delete r[morphIdCol];
946
+ if (morphTyCol)
947
+ delete r[morphTyCol];
948
+ // M2M pivot extras — flatten `row.pivot[col]` onto the row's data
949
+ // so each pivot column round-trips through the inner schema as a
950
+ // regular form field. The pivot envelope itself is dropped from
951
+ // the values shape — the persist side splits pivot vs child
952
+ // columns by name lookup against `cfg.pivotColumns`.
953
+ const pivotEnvelope = r['pivot'];
954
+ delete r['pivot'];
955
+ const stamped = { ...r };
956
+ if (pivotColumns && pivotColumns.length > 0
957
+ && pivotEnvelope && typeof pivotEnvelope === 'object') {
958
+ const pe = pivotEnvelope;
959
+ for (const col of pivotColumns) {
960
+ if (col in pe)
961
+ stamped[col] = pe[col];
962
+ }
963
+ }
964
+ if (pkValue !== undefined && pkValue !== null) {
965
+ stamped['__id'] = String(pkValue);
966
+ }
967
+ return stamped;
968
+ });
969
+ }
970
+ return out;
971
+ }
972
+ /** Walk the form's children for top-level relationship-backed Repeaters. */
973
+ function findRelationshipRepeaters(elements) {
974
+ const out = [];
975
+ const walk = (els) => {
976
+ for (const el of els) {
977
+ if (isRepeaterField(el)) {
978
+ const r = el;
979
+ if (r.getRelationship())
980
+ out.push(r);
981
+ // Don't dive into Repeater children — relationship-on-relationship
982
+ // isn't supported in v1.
983
+ continue;
984
+ }
985
+ // Don't dive into Builder children either — relationship-backed
986
+ // Builders are resolved separately by `findRelationshipBuilders`.
987
+ if (isBuilderField(el))
988
+ continue;
989
+ const children = el.getChildren();
990
+ if (children && children.length > 0)
991
+ walk(children);
992
+ }
993
+ };
994
+ walk(elements);
995
+ return out;
996
+ }
997
+ /**
998
+ * Walk the form's top-level Builders and replace `values[fieldName]` with
999
+ * rows fetched from `parent.related(name)` for any relationship-backed
1000
+ * Builder. Each loaded row stamps `__id` (child PK) + `type` (block
1001
+ * discriminator) + `data` (per-block JSON payload) so the renderer can
1002
+ * round-trip the heterogeneous envelope.
1003
+ *
1004
+ * Mirrors `applyRelationshipRepeaterFill`. No-op when the parent record
1005
+ * is null (create mode), the resource has no `R.model`, or no
1006
+ * relationship-backed Builders exist on the form.
1007
+ */
1008
+ export async function applyRelationshipBuilderFill(form, values, record, parentModel) {
1009
+ if (record == null)
1010
+ return values;
1011
+ if (!parentModel)
1012
+ return values;
1013
+ const builders = findRelationshipBuilders(form.getChildren() ?? []);
1014
+ if (builders.length === 0)
1015
+ return values;
1016
+ const out = { ...values };
1017
+ for (const builder of builders) {
1018
+ const cfg = builder.getRelationship();
1019
+ let rows;
1020
+ try {
1021
+ rows = await loadRelationRows(parentModel, record, cfg.name);
1022
+ }
1023
+ catch {
1024
+ // Failed lookup (e.g. missing `relations` map on a test stub) —
1025
+ // fall back to whatever value applyFillPipeline produced rather
1026
+ // than wiping the field. Better stale than silently empty.
1027
+ continue;
1028
+ }
1029
+ const pkColumn = pickChildPrimaryKey(parentModel, cfg.name) ?? 'id';
1030
+ const fkColumn = cfg.foreignKey ?? pickChildForeignKey(parentModel, cfg.name);
1031
+ const typeColumn = cfg.typeColumn ?? 'type';
1032
+ const dataColumn = cfg.dataColumn ?? 'data';
1033
+ out[builder.name] = rows.map(row => {
1034
+ const r = (row && typeof row === 'object') ? { ...row } : {};
1035
+ const pkValue = r[pkColumn];
1036
+ const blockType = typeof r[typeColumn] === 'string' ? r[typeColumn] : '';
1037
+ const dataRaw = r[dataColumn];
1038
+ const blockData = parseBuilderDataPayload(dataRaw);
1039
+ const stamped = {
1040
+ type: blockType,
1041
+ data: blockData,
1042
+ };
1043
+ if (pkValue !== undefined && pkValue !== null) {
1044
+ stamped['__id'] = String(pkValue);
1045
+ }
1046
+ // Non-`type` / `data` / FK / PK columns aren't surfaced — the
1047
+ // JSON envelope is the source of truth for per-block fields. If
1048
+ // a user denormalizes a column, they handle it via per-block
1049
+ // mutate hooks, not by leaking the column into row values.
1050
+ void fkColumn;
1051
+ return stamped;
1052
+ });
1053
+ }
1054
+ return out;
1055
+ }
1056
+ /**
1057
+ * Normalize the JSON payload column into a plain object. Prisma
1058
+ * hydrates `Json` columns to objects; some adapters return strings.
1059
+ * Anything that isn't a parseable object falls back to `{}` so the
1060
+ * inner schema renders fresh defaults.
1061
+ */
1062
+ function parseBuilderDataPayload(raw) {
1063
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
1064
+ return raw;
1065
+ }
1066
+ if (typeof raw === 'string') {
1067
+ try {
1068
+ const parsed = JSON.parse(raw);
1069
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1070
+ return parsed;
1071
+ }
1072
+ }
1073
+ catch {
1074
+ // fall through to {}
1075
+ }
1076
+ }
1077
+ return {};
1078
+ }
1079
+ /** Walk the form's children for top-level relationship-backed Builders. */
1080
+ function findRelationshipBuilders(elements) {
1081
+ const out = [];
1082
+ const walk = (els) => {
1083
+ for (const el of els) {
1084
+ if (isBuilderField(el)) {
1085
+ const b = el;
1086
+ if (b.getRelationship())
1087
+ out.push(b);
1088
+ continue;
1089
+ }
1090
+ // Don't dive into Repeater children either — both array-row
1091
+ // boundaries are walker stops here.
1092
+ if (isRepeaterField(el))
1093
+ continue;
1094
+ const children = el.getChildren();
1095
+ if (children && children.length > 0)
1096
+ walk(children);
1097
+ }
1098
+ };
1099
+ walk(elements);
1100
+ return out;
1101
+ }
1102
+ /** Read the child model's PK column from the parent's relations map, when present. */
1103
+ function pickChildPrimaryKey(parentModel, name) {
1104
+ const relations = parentModel['relations'];
1105
+ if (!relations || typeof relations !== 'object')
1106
+ return undefined;
1107
+ const entry = relations[name];
1108
+ if (!entry || typeof entry !== 'object')
1109
+ return undefined;
1110
+ const e = entry;
1111
+ if (typeof e['model'] !== 'function')
1112
+ return undefined;
1113
+ try {
1114
+ const child = e['model']();
1115
+ return getPrimaryKey(child);
1116
+ }
1117
+ catch {
1118
+ return undefined;
1119
+ }
1120
+ }
1121
+ /** Read the FK column from the parent's relations map, when present. */
1122
+ function pickChildForeignKey(parentModel, name) {
1123
+ const relations = parentModel['relations'];
1124
+ if (!relations || typeof relations !== 'object')
1125
+ return undefined;
1126
+ const entry = relations[name];
1127
+ if (!entry || typeof entry !== 'object')
1128
+ return undefined;
1129
+ const e = entry;
1130
+ return typeof e['foreignKey'] === 'string' ? e['foreignKey'] : undefined;
1131
+ }
1132
+ /**
1133
+ * Plan #15 — collect every `ServerDataElement` in the schema tree and
1134
+ * resolve their `getServerData(ctx)` payloads in parallel. Returns a
1135
+ * map keyed by element id, ready to ship as `viewProps._widgetData`.
1136
+ *
1137
+ * Lazy elements (default — `lazy(false)` opts out) skip the hook and
1138
+ * stamp `null` so the renderer paints a skeleton and fetches the
1139
+ * payload via `POST {base}/_widget/:id` on mount. Eager elements
1140
+ * resolve synchronously and ship the data with the page.
1141
+ *
1142
+ * Per-widget errors are caught and surfaced as `{ error: '...' }` —
1143
+ * one flaky `getStats()` shouldn't 500 the entire dashboard.
1144
+ *
1145
+ * Visibility is **not** re-evaluated here. The schema resolver
1146
+ * (`resolveSchema → evaluateVisibility`) drops hidden layout elements
1147
+ * before any widget code runs. Widgets inside still-rendered branches
1148
+ * always resolve (or stamp lazy null).
1149
+ */
1150
+ export async function resolveServerDataElements(elements, ctx) {
1151
+ const widgets = collectServerDataElements(elements);
1152
+ if (widgets.length === 0)
1153
+ return {};
1154
+ const out = {};
1155
+ await Promise.all(widgets.map(async (el) => {
1156
+ const id = el.getId();
1157
+ if (el.isLazy()) {
1158
+ out[id] = null; // sentinel — renderer paints skeleton, fetches on mount
1159
+ return;
1160
+ }
1161
+ try {
1162
+ out[id] = await el.resolveServerData(ctx);
1163
+ }
1164
+ catch (err) {
1165
+ out[id] = { error: err instanceof Error ? err.message : 'Widget failed to load' };
1166
+ }
1167
+ }));
1168
+ return out;
1169
+ }
1170
+ /** Walk the tree collecting every `ServerDataElement`. Walks into
1171
+ * containers but stops at Form/Repeater/Builder boundaries — widgets
1172
+ * inside an editable form don't make sense in v1. */
1173
+ function collectServerDataElements(elements) {
1174
+ const out = [];
1175
+ const walk = (els) => {
1176
+ for (const el of els) {
1177
+ if (isServerDataElement(el)) {
1178
+ out.push(el);
1179
+ // Don't recurse into a widget's children — `View` etc. are leaves
1180
+ // for v1 (no nested widgets inside widgets).
1181
+ continue;
1182
+ }
1183
+ // Skip walkers that imply per-row resolution — widgets inside
1184
+ // Repeater/Builder rows don't have a stable id space.
1185
+ const type = el.getType();
1186
+ if (type === 'form' || type === 'repeater' || type === 'builder' || type === 'table' || type === 'tableWidget')
1187
+ continue;
1188
+ const children = el.getChildren();
1189
+ if (children)
1190
+ walk(children);
1191
+ }
1192
+ };
1193
+ walk(elements);
1194
+ return out;
1195
+ }
1196
+ /**
1197
+ * Plan #15 — stamp the polling-endpoint URL on every `ServerDataElement`
1198
+ * in the tree. Mirrors `tagFormStateUrls / tagTableReorderUrls`. Walks
1199
+ * with the same boundaries as `collectServerDataElements` so the wire
1200
+ * stays in sync (no orphan widgets without URLs and vice versa).
1201
+ *
1202
+ * `urlBuilder(id)` typically produces `${base}/_widget/${id}` for
1203
+ * dashboard widgets and `${base}/${pageSlug}/_widget/${id}` for
1204
+ * custom-page widgets — the route handlers for both shapes are wired up
1205
+ * in `routes.ts` (see Phase A.4).
1206
+ */
1207
+ export function tagWidgetUrls(elements, urlBuilder) {
1208
+ for (const widget of collectServerDataElements(elements)) {
1209
+ if (widget.getWidgetUrl())
1210
+ continue; // user-set wins
1211
+ widget.withWidgetUrl(urlBuilder(widget.getId()));
1212
+ }
1213
+ }
1214
+ /** Stamp dispatchUrl on every handler-style Action so the client knows where to POST. */
1215
+ export function tagActionDispatch(elements, baseUrl) {
1216
+ for (const action of findActions(elements)) {
1217
+ if (!action.getHandler())
1218
+ continue;
1219
+ if (action.getHref() || action.getMethod())
1220
+ continue;
1221
+ if (action.getDispatchUrl())
1222
+ continue;
1223
+ action.dispatchUrl(`${baseUrl}/_action/${action.name}`);
1224
+ }
1225
+ // Row-scoped extraItemActions (Repeater/Builder). Stamped here too so
1226
+ // the client can POST to the same `_action/:name` route — the renderer
1227
+ // attaches `_rowPath=<fieldName>.<index>` per click; the server's
1228
+ // dispatcher uses that to walk into the right row when building
1229
+ // `ctx.row`. See `findRowExtraActions` in `dispatchAction.ts`.
1230
+ for (const { action } of findRowExtraActions(elements)) {
1231
+ if (!action.getHandler())
1232
+ continue;
1233
+ if (action.getDispatchUrl())
1234
+ continue;
1235
+ action.dispatchUrl(`${baseUrl}/_action/${action.name}`);
1236
+ }
1237
+ }
1238
+ // ─── Per-role data builders ──────────────────────────────────
1239
+ export async function dashboardData(pilotiq, req) {
1240
+ const cfg = pilotiq.getConfig();
1241
+ const user = await pilotiq.resolveUser(req);
1242
+ const ctx = uploadCtx(userCtx({ basePath: cfg.path }, user), cfg);
1243
+ // Plan #15 — when `panel.dashboard(P)` was called, resolve P's
1244
+ // schema instead of the builder-level `cfg.schema`. Page-scoped
1245
+ // schema means widget elements read like a regular custom page —
1246
+ // including action dispatch, form-state, and `_widget/:id` polling.
1247
+ let elements;
1248
+ if (cfg.dashboardPage) {
1249
+ elements = await callPageSchema(cfg.dashboardPage, ctx);
1250
+ tagFormActions(elements, cfg.path);
1251
+ tagFormStateUrls(elements, formId => `${cfg.path}/_form/${formId}/state`);
1252
+ tagFormWizardUrls(elements, formId => `${cfg.path}/_form/${formId}/wizard`);
1253
+ tagRichTextMentionUrls(elements, formId => `${cfg.path}/_form/${formId}/mentions`);
1254
+ tagSelectCreateOptionUrls(elements, (formId, fieldName) => `${cfg.path}/_form/${formId}/create-option/${fieldName}`);
1255
+ tagActionDispatch(elements, cfg.path);
1256
+ }
1257
+ else {
1258
+ elements = [];
1259
+ if (cfg.schema) {
1260
+ const def = cfg.schema;
1261
+ elements = typeof def === 'function' ? await def(ctx) : def;
1262
+ }
1263
+ }
1264
+ // Stamp polling URLs on every widget — panel-scope (no pageSlug
1265
+ // segment) for the dashboard. Done before schema resolve so the URL
1266
+ // rides on each widget's stamped meta.
1267
+ tagWidgetUrls(elements, id => `${cfg.path}/_widget/${id}`);
1268
+ const widgetData = await resolveServerDataElements(elements, ctx);
1269
+ const dashRoute = cfg.dashboardPage ? { page: cfg.dashboardPage } : {};
1270
+ const schemaData = await applyRoleHooks(pilotiq, user, 'dashboard', await resolveSchema(elements, ctx), dashRoute);
1271
+ return {
1272
+ panel: await panelInfo(pilotiq, req, dashRoute),
1273
+ page: cfg.dashboardPage ? cfg.dashboardPage.toMeta() : undefined,
1274
+ basePath: cfg.path,
1275
+ layout: cfg.layout,
1276
+ schemaData,
1277
+ _widgetData: widgetData,
1278
+ notifications: consumeFlashedNotifications(req),
1279
+ };
1280
+ }
1281
+ export async function resourceIndexData(pilotiq, slug, query = {}, req) {
1282
+ const cfg = pilotiq.getConfig();
1283
+ const R = cfg.resources.find(r => r.getSlug() === slug);
1284
+ if (!R)
1285
+ return null;
1286
+ const pages = R.resolvePages();
1287
+ if (!pages.index)
1288
+ return null;
1289
+ const PageClass = pages.index;
1290
+ const indexUrl = resourceBasePath(cfg.path, R);
1291
+ const user = await pilotiq.resolveUser(req);
1292
+ const ctx = uploadCtx(userCtx({ mode: 'table', basePath: cfg.path }, user), cfg);
1293
+ const elements = await callPageSchema(PageClass, ctx);
1294
+ tagActionDispatch(elements, indexUrl);
1295
+ // Plan #15 — resource-scope widget polling URL. Stamped before the
1296
+ // schema resolves so each widget's meta carries its endpoint.
1297
+ tagWidgetUrls(elements, id => `${indexUrl}/_widget/${id}`);
1298
+ // Mark the active tab + parallel-eval badges + stamp per-tab URLs
1299
+ // before the table records run — `loadTableRecords` walks the schema
1300
+ // for the active tab and splices its `modifyQuery` predicate into the
1301
+ // ORM chain alongside filters.
1302
+ await resolveActiveTab(elements, query, indexUrl);
1303
+ if (R.deferLoading)
1304
+ tagTableDeferred(elements, `${indexUrl}/_table`);
1305
+ await loadTableRecords(elements, query, indexUrl, user, {
1306
+ canEdit: (u, record) => R.canEdit(u, record),
1307
+ });
1308
+ tagTableReorderUrls(elements, `${indexUrl}/_reorder`);
1309
+ tagCellEditUrls(elements, indexUrl);
1310
+ const widgetData = await resolveServerDataElements(elements, ctx);
1311
+ const breadcrumbs = resourceListBreadcrumbs(cfg, R);
1312
+ if (breadcrumbs)
1313
+ elements.unshift(breadcrumbs);
1314
+ const listRoute = { resource: R, page: PageClass };
1315
+ const schemaData = await applyRoleHooks(pilotiq, user, 'list', await resolveSchema(elements, ctx), listRoute);
1316
+ return {
1317
+ pageType: 'resource',
1318
+ panel: await panelInfo(pilotiq, req, listRoute),
1319
+ page: PageClass.toMeta(),
1320
+ resource: { name: R.name, label: R.label, labelSingular: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
1321
+ basePath: cfg.path,
1322
+ layout: cfg.layout,
1323
+ schemaData,
1324
+ _widgetData: widgetData,
1325
+ notifications: consumeFlashedNotifications(req),
1326
+ };
1327
+ }
1328
+ // Deferred-load JSON endpoint payload — `GET {base}/{slug}/_table`
1329
+ // re-runs the list-page builder without the deferred flag, then returns
1330
+ // every resolved `TableMeta` as a flat array. Returns null on missing
1331
+ // resource / index page (route 404s).
1332
+ export async function resourceTableData(pilotiq, slug, query = {}, req) {
1333
+ const cfg = pilotiq.getConfig();
1334
+ const R = cfg.resources.find(r => r.getSlug() === slug);
1335
+ if (!R)
1336
+ return null;
1337
+ const pages = R.resolvePages();
1338
+ if (!pages.index)
1339
+ return null;
1340
+ const PageClass = pages.index;
1341
+ const indexUrl = resourceBasePath(cfg.path, R);
1342
+ const user = await pilotiq.resolveUser(req);
1343
+ const ctx = uploadCtx(userCtx({ mode: 'table', basePath: cfg.path }, user), cfg);
1344
+ const elements = await callPageSchema(PageClass, ctx);
1345
+ tagActionDispatch(elements, indexUrl);
1346
+ await resolveActiveTab(elements, query, indexUrl);
1347
+ await loadTableRecords(elements, query, indexUrl, user, {
1348
+ canEdit: (u, record) => R.canEdit(u, record),
1349
+ });
1350
+ tagTableReorderUrls(elements, `${indexUrl}/_reorder`);
1351
+ tagCellEditUrls(elements, indexUrl);
1352
+ const schemaData = await resolveSchema(elements, ctx);
1353
+ const tables = collectTableMetas(schemaData);
1354
+ return { tables };
1355
+ }
1356
+ function collectTableMetas(metas) {
1357
+ const out = [];
1358
+ const walk = (nodes) => {
1359
+ for (const node of nodes) {
1360
+ if (node['type'] === 'table')
1361
+ out.push(node);
1362
+ const children = node['children'];
1363
+ if (Array.isArray(children))
1364
+ walk(children);
1365
+ }
1366
+ };
1367
+ walk(metas);
1368
+ return out;
1369
+ }
1370
+ /**
1371
+ * Walk the schema for `ListTabs` containers, pick the active tab from
1372
+ * `?tab=…` (defaulting to the tab marked `.default()` or the first one),
1373
+ * stamp render-time state (`active` flag, per-tab `?tab=` URL, and
1374
+ * resolved badge counts) onto each tab. The active tab's query/context
1375
+ * modifier is NOT applied here — `loadTableRecords` walks for the active
1376
+ * tab and splices in its modifier when it builds the records-handler
1377
+ * `TableContext`.
1378
+ *
1379
+ * No-op when the page has no `ListTabs`.
1380
+ */
1381
+ export async function resolveActiveTab(elements, query, currentPath) {
1382
+ const listTabs = findListTabs(elements);
1383
+ if (listTabs.length === 0)
1384
+ return;
1385
+ for (const container of listTabs) {
1386
+ const children = (container.getChildren() ?? []).filter((c) => c.getType() === 'listTab');
1387
+ if (children.length === 0)
1388
+ continue;
1389
+ // Default tab (used both for `?tab=` fallback and to omit the param
1390
+ // from the canonical URL of that tab — see `buildTabUrl`).
1391
+ const defaultTab = children.find(t => t.isDefault()) ?? children[0];
1392
+ // Active tab: explicit `?tab=name` → default tab.
1393
+ const wanted = typeof query['tab'] === 'string' ? query['tab'] : undefined;
1394
+ const active = (wanted && children.find(t => t.name === wanted)) || defaultTab;
1395
+ // Stamp render-time state on each tab.
1396
+ children.forEach(t => {
1397
+ t.withActive(t === active);
1398
+ t.withUrl(buildTabUrl(currentPath, query, t.name, defaultTab.name));
1399
+ });
1400
+ // Resolve every tab's badge in parallel — failed handlers swallow
1401
+ // silently (badge omitted) so a flaky count never blanks the page.
1402
+ await Promise.all(children.map(async (tab) => {
1403
+ const handler = tab.getBadgeHandler();
1404
+ if (!handler)
1405
+ return;
1406
+ try {
1407
+ const v = await handler();
1408
+ if (v === undefined || v === null)
1409
+ return;
1410
+ tab.withResolvedBadge(String(v));
1411
+ }
1412
+ catch {
1413
+ // Per-tab badge errors stay silent.
1414
+ }
1415
+ }));
1416
+ }
1417
+ }
1418
+ function findListTabs(elements) {
1419
+ const out = [];
1420
+ const walk = (els) => {
1421
+ for (const el of els) {
1422
+ if (el.getType() === 'listTabs')
1423
+ out.push(el);
1424
+ const children = el.getChildren();
1425
+ if (children)
1426
+ walk(children);
1427
+ }
1428
+ };
1429
+ walk(elements);
1430
+ return out;
1431
+ }
1432
+ function buildTabUrl(pathname, query, tabName, defaultTabName) {
1433
+ // Carry forward search/sort/perPage + any filter values; reset page to 1
1434
+ // (tab change reshapes the result set, page numbers don't translate).
1435
+ // The default tab gets the canonical, paramless URL — visiting that URL
1436
+ // already lands on the default, so emitting `?tab=default` would just be
1437
+ // noise that bookmarks/share-links pick up.
1438
+ const params = new URLSearchParams();
1439
+ for (const [k, v] of Object.entries(query)) {
1440
+ if (v === undefined || v === '' || v === null)
1441
+ continue;
1442
+ if (k === 'tab' || k === 'page')
1443
+ continue;
1444
+ params.set(k, String(v));
1445
+ }
1446
+ if (tabName !== defaultTabName)
1447
+ params.set('tab', tabName);
1448
+ const qs = params.toString();
1449
+ return qs ? `${pathname}?${qs}` : pathname;
1450
+ }
1451
+ export async function resourceCreateData(pilotiq, slug, prefill, req) {
1452
+ const cfg = pilotiq.getConfig();
1453
+ const R = cfg.resources.find(r => r.getSlug() === slug);
1454
+ if (!R)
1455
+ return null;
1456
+ const pages = R.resolvePages();
1457
+ if (!pages.create)
1458
+ return null;
1459
+ const PageClass = pages.create;
1460
+ const resourceBase = resourceBasePath(cfg.path, R);
1461
+ const createUrl = `${resourceBase}/create`;
1462
+ const user = await pilotiq.resolveUser(req);
1463
+ const ctx = uploadCtx(userCtx({ mode: 'create', basePath: cfg.path }, user), cfg);
1464
+ const elements = await callPageSchema(PageClass, ctx);
1465
+ tagFormActions(elements, createUrl);
1466
+ tagActionDispatch(elements, createUrl);
1467
+ tagFormStateUrls(elements, formId => `${resourceBase}/_form/${formId}/state`);
1468
+ tagFormWizardUrls(elements, formId => `${resourceBase}/_form/${formId}/wizard`);
1469
+ tagRichTextMentionUrls(elements, formId => `${resourceBase}/_form/${formId}/mentions`);
1470
+ tagSelectCreateOptionUrls(elements, (formId, fieldName) => `${resourceBase}/_form/${formId}/create-option/${fieldName}`);
1471
+ if (prefill) {
1472
+ const form = findForms(elements)[0];
1473
+ if (form) {
1474
+ if (prefill.values)
1475
+ form.withValues(prefill.values);
1476
+ if (prefill.errors)
1477
+ form.withErrors(prefill.errors);
1478
+ }
1479
+ }
1480
+ const breadcrumbs = resourceCreateBreadcrumbs(cfg, R);
1481
+ if (breadcrumbs)
1482
+ elements.unshift(breadcrumbs);
1483
+ const createRoute = { resource: R, page: PageClass };
1484
+ const schemaData = await applyRoleHooks(pilotiq, user, 'create', await resolveSchema(elements, ctx), createRoute);
1485
+ return {
1486
+ panel: await panelInfo(pilotiq, req, createRoute),
1487
+ page: PageClass.toMeta(),
1488
+ resource: { name: R.name, label: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
1489
+ mode: 'create',
1490
+ basePath: cfg.path,
1491
+ layout: cfg.layout,
1492
+ schemaData,
1493
+ notifications: consumeFlashedNotifications(req),
1494
+ ...(prefill?.errors ? { hasErrors: true } : {}),
1495
+ };
1496
+ }
1497
+ export async function resourceEditData(pilotiq, slug, recordId, prefill, req) {
1498
+ const cfg = pilotiq.getConfig();
1499
+ const R = cfg.resources.find(r => r.getSlug() === slug);
1500
+ if (!R)
1501
+ return null;
1502
+ const pages = R.resolvePages();
1503
+ if (!pages.edit)
1504
+ return null;
1505
+ const PageClass = pages.edit;
1506
+ const resourceBase = resourceBasePath(cfg.path, R);
1507
+ const editUrl = `${resourceBase}/${recordId}/edit`;
1508
+ const user = await pilotiq.resolveUser(req);
1509
+ const ctx = uploadCtx(userCtx({ mode: 'edit', recordId, basePath: cfg.path }, user), cfg);
1510
+ const elements = await callPageSchema(PageClass, ctx);
1511
+ tagFormActions(elements, editUrl);
1512
+ tagActionDispatch(elements, editUrl);
1513
+ tagFormStateUrls(elements, formId => `${resourceBase}/${recordId}/_form/${formId}/state`);
1514
+ tagFormWizardUrls(elements, formId => `${resourceBase}/${recordId}/_form/${formId}/wizard`);
1515
+ tagRichTextMentionUrls(elements, formId => `${resourceBase}/${recordId}/_form/${formId}/mentions`);
1516
+ tagSelectCreateOptionUrls(elements, (formId, fieldName) => `${resourceBase}/${recordId}/_form/${formId}/create-option/${fieldName}`);
1517
+ // Locate the primary form, load the record, fill values.
1518
+ const form = findForms(elements)[0];
1519
+ let record = undefined;
1520
+ if (form?.getLoadRecord()) {
1521
+ try {
1522
+ record = await form.getLoadRecord()(recordId, { values: prefill?.values ?? {} });
1523
+ }
1524
+ catch {
1525
+ // sentinel/missing record — fall through
1526
+ }
1527
+ if (!prefill?.values && record != null) {
1528
+ const values = await applyFillPipeline(form, record);
1529
+ const withRelations = await applyRelationshipRepeaterFill(form, values, record, R.model);
1530
+ const withBuilders = await applyRelationshipBuilderFill(form, withRelations, record, R.model);
1531
+ form.withValues(withBuilders);
1532
+ }
1533
+ else if (prefill?.values) {
1534
+ form.withValues(prefill.values);
1535
+ }
1536
+ if (prefill?.errors)
1537
+ form.withErrors(prefill.errors);
1538
+ }
1539
+ // Plan #11 — when the resource has relation managers, prepend a
1540
+ // navigation strip so users can drill into each manager's table
1541
+ // without leaving the parent record context. The "Edit" tab is
1542
+ // active here.
1543
+ const relationTabsEl = buildRelationTabs(R, recordId, cfg.path, '__edit');
1544
+ if (relationTabsEl)
1545
+ elements.unshift(relationTabsEl);
1546
+ const recordTitle = record !== undefined && record !== null
1547
+ ? deriveParentTitle(R, record)
1548
+ : recordId;
1549
+ const breadcrumbs = resourceEditBreadcrumbs(cfg, R, recordId, recordTitle);
1550
+ if (breadcrumbs)
1551
+ elements.unshift(breadcrumbs);
1552
+ const editRoute = { resource: R, page: PageClass, recordId };
1553
+ const schemaData = await applyRoleHooks(pilotiq, user, 'edit', await resolveSchema(elements, record !== undefined ? { ...ctx, record } : ctx), editRoute);
1554
+ return {
1555
+ panel: await panelInfo(pilotiq, req, editRoute),
1556
+ page: PageClass.toMeta(),
1557
+ resource: { name: R.name, label: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
1558
+ mode: 'edit',
1559
+ recordId,
1560
+ basePath: cfg.path,
1561
+ layout: cfg.layout,
1562
+ schemaData,
1563
+ notifications: consumeFlashedNotifications(req),
1564
+ ...(prefill?.errors ? { hasErrors: true } : {}),
1565
+ };
1566
+ }
1567
+ /**
1568
+ * Discover the related Resource for a manager. Order:
1569
+ * 1. `M.relatedResource` explicit override (skip discovery).
1570
+ * 2. Rudder ORM convention: walk
1571
+ * `R.model.relations[manager.relationship].model()` and find
1572
+ * `cfg.resources[i].model === relatedModel`.
1573
+ * 3. Otherwise undefined — caller must error or fall back.
1574
+ *
1575
+ * A returned Resource is the one whose `model` backs the related
1576
+ * table. Callers use it for `Related.model.find(childId)`,
1577
+ * `Related.canEdit(user, child)`, and the auto-wired form save handler.
1578
+ */
1579
+ export function findRelatedResource(M, R, cfg) {
1580
+ if (M.relatedResource)
1581
+ return M.relatedResource;
1582
+ const ParentModel = R.model;
1583
+ if (!ParentModel)
1584
+ return undefined;
1585
+ const def = ParentModel.relations?.[M.getRelationship()];
1586
+ const RelatedModel = typeof def?.model === 'function' ? def.model() : undefined;
1587
+ if (!RelatedModel)
1588
+ return undefined;
1589
+ return cfg.resources.find(r => r.model === RelatedModel);
1590
+ }
1591
+ /** Find a registered manager on a Resource by its relationship key.
1592
+ * Throws on unknown manager — so the route can 404 cleanly. */
1593
+ function findManager(R, relationship) {
1594
+ return R.relations().find(M => {
1595
+ try {
1596
+ return M.getRelationship() === relationship;
1597
+ }
1598
+ catch {
1599
+ return false;
1600
+ }
1601
+ });
1602
+ }
1603
+ /**
1604
+ * Verify a child record actually belongs to the given parent under the
1605
+ * declared relationship. Anti-IDOR — without this an attacker can swap
1606
+ * the `:childId` segment to load any related-model row regardless of
1607
+ * whether it's actually owned by the parent.
1608
+ *
1609
+ * Strategy: re-resolve the parent's relation query and check whether
1610
+ * the child's primary key shows up in `where(pk, '=', childId).paginate(1, 1)`.
1611
+ * Yes, it's a second round-trip — but it's the single point of trust
1612
+ * for IDOR safety, and it fits naturally into the same query path
1613
+ * `modelRelationTableRecords` uses.
1614
+ */
1615
+ async function childBelongsToParent(parentModel, parent, relationship, childPk, childId) {
1616
+ try {
1617
+ const q = (parentModel.relatedQuery
1618
+ ? parentModel.relatedQuery(parent, relationship)
1619
+ : parent.related(relationship));
1620
+ const result = await q.where(childPk, '=', childId).paginate(1, 1);
1621
+ return result.total > 0;
1622
+ }
1623
+ catch {
1624
+ return false;
1625
+ }
1626
+ }
1627
+ /**
1628
+ * Auto-wire the manager's table records loader against the parent's
1629
+ * relation query when the user didn't set `Table.records()` themselves.
1630
+ * Mirrors `defaultPages`'s wiring of `Table.records()` from `R.model`
1631
+ * for the resource list page.
1632
+ */
1633
+ function autoWireManagerTable(table, parentModel, parent, relationship) {
1634
+ if (table.getRecords())
1635
+ return; // user wired it explicitly
1636
+ table.records(modelRelationTableRecords(parentModel, parent, relationship, table));
1637
+ }
1638
+ /**
1639
+ * Plan #13 polish — auto-inject `TrashedFilter` on a relation manager's
1640
+ * table when the **related** Resource opts into soft deletes. Mirrors the
1641
+ * resource-list pattern in `defaultPages.applyTableDefaults`. The check
1642
+ * is on the related Resource (not the manager), because soft-delete is a
1643
+ * model-level capability — if the child model supports trashing, the
1644
+ * manager's table should expose the toggle.
1645
+ *
1646
+ * No-op when:
1647
+ * - the related Resource hasn't set `softDeletes = true`
1648
+ * - the user already attached a `TrashedFilter` in `M.table()`
1649
+ */
1650
+ function injectManagerTrashedFilter(table, Related) {
1651
+ if (!Related?.softDeletes)
1652
+ return;
1653
+ const children = table.getChildren() ?? [];
1654
+ const hasTrashed = children.some(c => c instanceof TrashedFilter);
1655
+ if (hasTrashed)
1656
+ return;
1657
+ const existing = children.filter(c => c instanceof Filter);
1658
+ table.filters([...existing, TrashedFilter.make()]);
1659
+ }
1660
+ /**
1661
+ * Auto-wire the manager's form save + loadRecord handlers against the
1662
+ * **related** Resource's `model` when the user didn't set them. The
1663
+ * route handler is responsible for stamping the parent context
1664
+ * (parent, parentRecord, parentId, relationship) onto the
1665
+ * `FormContext` so user-supplied `mutateDataBeforeCreate` etc. can
1666
+ * read them.
1667
+ */
1668
+ function autoWireManagerForm(form, Related) {
1669
+ const RelatedModel = Related.model;
1670
+ if (!RelatedModel)
1671
+ return;
1672
+ if (!form.getSave())
1673
+ form.save(modelSave(RelatedModel));
1674
+ if (!form.getLoadRecord())
1675
+ form.loadRecord(modelLoadRecord(Related));
1676
+ }
1677
+ async function safePolicy(fn) {
1678
+ try {
1679
+ return Boolean(await fn());
1680
+ }
1681
+ catch {
1682
+ return false;
1683
+ }
1684
+ }
1685
+ /** Plan #11 — authorize a relation-manager action with sensible defaults.
1686
+ * Re-exported from `RelationManager.ts` so external callers (route
1687
+ * handlers, third-party plugins) keep their existing import path. */
1688
+ export const safeManagerPolicy = safeManagerPolicyImpl;
1689
+ /**
1690
+ * Plan #11 — render data for the three relation-manager URL scopes.
1691
+ * Mirrors the resource* builders' shape so routes and Vike +data hooks
1692
+ * consume identical props. Authorization runs inline (parent
1693
+ * `canAccess + canEdit(parent)` then manager-scoped predicate); IDOR
1694
+ * check on `relation-edit` runs against the parent's relation query.
1695
+ *
1696
+ * Returns:
1697
+ * - `null` when panel / parent / manager / child don't exist.
1698
+ * - `{ ok: false, status: 403 }` when authorization denies.
1699
+ * - the props record on success (route picks SSR view / SPA prop
1700
+ * downstream).
1701
+ */
1702
+ export async function relationManagerData(pilotiq, scope, req) {
1703
+ // Phase B nested-relation-* scopes split out into their own pipeline
1704
+ // — the chain walking + per-layer auth differs enough from the
1705
+ // depth-1 path that interleaving them would mostly hurt readability.
1706
+ if (scope.kind === 'nested-relation-list'
1707
+ || scope.kind === 'nested-relation-create'
1708
+ || scope.kind === 'nested-relation-view'
1709
+ || scope.kind === 'nested-relation-edit') {
1710
+ return nestedRelationManagerData(pilotiq, scope, req);
1711
+ }
1712
+ const cfg = pilotiq.getConfig();
1713
+ const R = cfg.resources.find(r => r.getSlug() === scope.slug);
1714
+ if (!R)
1715
+ return null;
1716
+ const M = findManager(R, scope.relationship);
1717
+ if (!M)
1718
+ return null;
1719
+ const user = await pilotiq.resolveUser(req);
1720
+ // Layer 1: parent access. canAccess gates the resource entirely;
1721
+ // canEdit gates managing its relations (managers are read-write
1722
+ // surfaces — read-only inline views opt in by overriding the
1723
+ // manager's can*). Cluster gate composes with R.canAccess — both
1724
+ // must pass when the parent resource is inside a cluster.
1725
+ if (R.cluster && !await safePolicy(() => R.cluster.canAccess(user)))
1726
+ return { ok: false, status: 403 };
1727
+ if (!await safePolicy(() => R.canAccess(user)))
1728
+ return { ok: false, status: 403 };
1729
+ if (!R.model) {
1730
+ // Without a model on the parent we can't load the parent record,
1731
+ // and without that we can't IDOR-check children. Point users at
1732
+ // the missing wiring rather than silent 500s.
1733
+ throw new Error(`[Pilotiq] Resource "${R.name}" has relations(${M.name}) but no static model. ` +
1734
+ `Set Resource.model = … to enable relation managers, or remove the manager.`);
1735
+ }
1736
+ const parentRecord = await findRecord(R, scope.recordId, { user }).catch(() => undefined);
1737
+ if (!parentRecord)
1738
+ return null;
1739
+ if (!await safePolicy(() => R.canEdit(user, parentRecord)))
1740
+ return { ok: false, status: 403 };
1741
+ // Read the relation type off the parent's relations map once,
1742
+ // normalize to the six-way `RelationMode` the manager-side logic
1743
+ // uses. `belongsToMany` / `morphToMany` (owning polymorphic) /
1744
+ // `morphedByMany` (inverse polymorphic) all flip into pivot-mutation
1745
+ // mode (attach / detach / sync — same accessor surface), `morphMany|
1746
+ // morphOne` collapses to `'morphMany'` (parent-side polymorphic —
1747
+ // auto-fills morph columns on create), `morphTo` is the child-side
1748
+ // polymorphic (no auto-actions; requires explicit `M.relatedResource`).
1749
+ // Everything else collapses to `'hasMany'`.
1750
+ const relationType = getRelationType(R.model, scope.relationship);
1751
+ const mode = normalizeRelationMode(relationType);
1752
+ const Related = findRelatedResource(M, R, cfg);
1753
+ // Related Resource is required for: edit/create form auto-wire,
1754
+ // child loading on edit, related URL generation. Throw when missing
1755
+ // *only* if we'd otherwise need it — for `relation-list` it's
1756
+ // optional (the table can be hand-wired by the user).
1757
+ const needRelated = scope.kind !== 'relation-list';
1758
+ if (needRelated && !Related) {
1759
+ throw new Error(`[Pilotiq] RelationManager ${M.name} on ${R.name} could not resolve its related Resource. ` +
1760
+ `Set static relatedResource on the manager, or ensure the parent's model declares relations[${JSON.stringify(M.getRelationship())}].`);
1761
+ }
1762
+ switch (scope.kind) {
1763
+ case 'relation-list':
1764
+ return buildRelationListData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode);
1765
+ case 'relation-create':
1766
+ return buildRelationCreateData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode);
1767
+ case 'relation-view':
1768
+ return buildRelationViewData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode);
1769
+ case 'relation-edit':
1770
+ return buildRelationEditData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode);
1771
+ }
1772
+ }
1773
+ async function buildRelationListData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode) {
1774
+ if (!await safeManagerPolicy(M, 'canViewAny', Related, user, parentRecord))
1775
+ return { ok: false, status: 403 };
1776
+ const cfg = pilotiq.getConfig();
1777
+ const base = cfg.path;
1778
+ const resourceBase = resourceBasePath(base, R);
1779
+ const listUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}`;
1780
+ // Build a single Table by piping a fresh Table through M.table(table, ctx).
1781
+ // Context lets the user wire `Action.relationCreate / relationEdit /
1782
+ // relationDelete(M, ctx)` factories inside `static table()` to template
1783
+ // URLs without threading basePath / parentId by hand.
1784
+ const managerCtx = {
1785
+ basePath: base,
1786
+ parentSlug: scope.slug,
1787
+ parentId: scope.recordId,
1788
+ relationship: scope.relationship,
1789
+ parentRecord,
1790
+ related: Related,
1791
+ mode,
1792
+ };
1793
+ const table = M.table(Table.make(), managerCtx);
1794
+ autoWireManagerTable(table, R.model, parentRecord, scope.relationship);
1795
+ injectManagerTrashedFilter(table, Related);
1796
+ const ctx = uploadCtx(userCtx({
1797
+ mode: 'table',
1798
+ basePath: base,
1799
+ record: parentRecord,
1800
+ }, user), cfg);
1801
+ const elements = [table];
1802
+ tagActionDispatch(elements, listUrl);
1803
+ await loadTableRecords(elements, scope.query ?? {}, listUrl, user);
1804
+ const tabs = buildRelationTabs(R, scope.recordId, base, scope.relationship);
1805
+ if (tabs)
1806
+ elements.unshift(tabs);
1807
+ const breadcrumbs = relationListBreadcrumbs(cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord));
1808
+ if (breadcrumbs)
1809
+ elements.unshift(breadcrumbs);
1810
+ const relationListRoute = { resource: R, recordId: scope.recordId };
1811
+ const schemaData = await applyRoleHooks(pilotiq, user, 'relation-list', await resolveSchema(elements, ctx), relationListRoute);
1812
+ return {
1813
+ pageType: 'relation-list',
1814
+ panel: await panelInfo(pilotiq, req, relationListRoute),
1815
+ resource: { name: R.name, label: R.label, labelSingular: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
1816
+ relation: {
1817
+ name: M.name,
1818
+ label: M.getLabel(),
1819
+ labelSingular: M.getLabelSingular(),
1820
+ relationship: scope.relationship,
1821
+ icon: M.getIcon() ? serializeIcon(M.getIcon(), M.name) : undefined,
1822
+ relatedSlug: Related?.getSlug(),
1823
+ },
1824
+ parent: {
1825
+ id: scope.recordId,
1826
+ title: deriveParentTitle(R, parentRecord),
1827
+ },
1828
+ basePath: base,
1829
+ layout: cfg.layout,
1830
+ schemaData,
1831
+ notifications: consumeFlashedNotifications(req),
1832
+ };
1833
+ }
1834
+ async function buildRelationCreateData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode) {
1835
+ if (!await safeManagerPolicy(M, 'canCreate', Related, user, parentRecord))
1836
+ return { ok: false, status: 403 };
1837
+ const cfg = pilotiq.getConfig();
1838
+ const base = cfg.path;
1839
+ const resourceBase = resourceBasePath(base, R);
1840
+ const createUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}/create`;
1841
+ const managerCtx = {
1842
+ basePath: base,
1843
+ parentSlug: scope.slug,
1844
+ parentId: scope.recordId,
1845
+ relationship: scope.relationship,
1846
+ parentRecord,
1847
+ related: Related,
1848
+ mode,
1849
+ };
1850
+ const form = M.form(Form.make(), managerCtx);
1851
+ if (Related.model)
1852
+ autoWireManagerForm(form, Related);
1853
+ const elements = [form];
1854
+ tagFormActions(elements, createUrl);
1855
+ if (scope.prefill) {
1856
+ if (scope.prefill.values)
1857
+ form.withValues(scope.prefill.values);
1858
+ if (scope.prefill.errors)
1859
+ form.withErrors(scope.prefill.errors);
1860
+ }
1861
+ const tabs = buildRelationTabs(R, scope.recordId, base, scope.relationship);
1862
+ if (tabs)
1863
+ elements.unshift(tabs);
1864
+ const breadcrumbs = relationCreateBreadcrumbs(cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord));
1865
+ if (breadcrumbs)
1866
+ elements.unshift(breadcrumbs);
1867
+ const ctx = uploadCtx(userCtx({
1868
+ mode: 'create',
1869
+ basePath: base,
1870
+ record: parentRecord,
1871
+ }, user), cfg);
1872
+ const relationCreateRoute = { resource: R, recordId: scope.recordId };
1873
+ const schemaData = await applyRoleHooks(pilotiq, user, 'relation-create', await resolveSchema(elements, ctx), relationCreateRoute);
1874
+ return {
1875
+ pageType: 'relation-create',
1876
+ panel: await panelInfo(pilotiq, req, relationCreateRoute),
1877
+ resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
1878
+ relation: {
1879
+ name: M.name,
1880
+ label: M.getLabel(),
1881
+ labelSingular: M.getLabelSingular(),
1882
+ relationship: scope.relationship,
1883
+ icon: M.getIcon() ? serializeIcon(M.getIcon(), M.name) : undefined,
1884
+ relatedSlug: Related.getSlug(),
1885
+ },
1886
+ parent: {
1887
+ id: scope.recordId,
1888
+ title: deriveParentTitle(R, parentRecord),
1889
+ },
1890
+ mode: 'create',
1891
+ basePath: base,
1892
+ layout: cfg.layout,
1893
+ schemaData,
1894
+ notifications: consumeFlashedNotifications(req),
1895
+ ...(scope.prefill?.errors ? { hasErrors: true } : {}),
1896
+ };
1897
+ }
1898
+ /**
1899
+ * Phase A — read-only view page for a related record at depth-2:
1900
+ * `${base}/${slug}/:id/${rel}/:childId`. Mirrors `buildRelationEditData`'s
1901
+ * IDOR + auth posture but resolves the manager's `static detail(child,
1902
+ * parent)` instead of its form. The default `detail()` returns `[]` —
1903
+ * managers opt in by overriding it; the chrome (RelationTabs strip)
1904
+ * still renders so users can sideways-nav between sibling managers.
1905
+ */
1906
+ async function buildRelationViewData(pilotiq, R, M, Related, parentRecord, scope, req, user, _mode) {
1907
+ if (!Related.model) {
1908
+ throw new Error(`[Pilotiq] Cannot load child record for ${M.name}: Related Resource ${Related.name} has no static model.`);
1909
+ }
1910
+ const childPk = getPrimaryKey(Related.model);
1911
+ const belongs = await childBelongsToParent(R.model, parentRecord, scope.relationship, childPk, scope.childId);
1912
+ if (!belongs)
1913
+ return null;
1914
+ const child = await findRecord(Related, scope.childId, { user }).catch(() => undefined);
1915
+ if (!child)
1916
+ return null;
1917
+ if (!await safeManagerPolicy(M, 'canView', Related, user, parentRecord, child))
1918
+ return { ok: false, status: 403 };
1919
+ const cfg = pilotiq.getConfig();
1920
+ const base = cfg.path;
1921
+ const elements = M.detail(child, parentRecord);
1922
+ // Phase B polish — when M declares nested managers, surface them on
1923
+ // this page too. The strip lists the leaf parent's view tab plus one
1924
+ // tab per sibling nested manager so users can jump from the Phase A
1925
+ // view straight into a grandchild list / create / view / edit page.
1926
+ // Active key `'__view'` because the user is currently viewing the
1927
+ // leaf parent record itself, not any nested manager.
1928
+ const nestedTabs = buildNestedRelationTabs(R, M, base, { recordId: scope.recordId, relationship: scope.relationship }, scope.childId, '__view');
1929
+ if (nestedTabs)
1930
+ elements.unshift(nestedTabs);
1931
+ const tabs = buildRelationTabs(R, scope.recordId, base, scope.relationship);
1932
+ if (tabs)
1933
+ elements.unshift(tabs);
1934
+ const breadcrumbs = relationViewBreadcrumbs(cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord), deriveParentTitle(Related, child, M));
1935
+ if (breadcrumbs)
1936
+ elements.unshift(breadcrumbs);
1937
+ const ctx = uploadCtx(userCtx({
1938
+ mode: 'view',
1939
+ basePath: base,
1940
+ record: child,
1941
+ recordId: scope.childId,
1942
+ }, user), cfg);
1943
+ const relationViewRoute = { resource: R, recordId: scope.childId };
1944
+ const schemaData = await applyRoleHooks(pilotiq, user, 'relation-view', await resolveSchema(elements, ctx), relationViewRoute);
1945
+ return {
1946
+ pageType: 'relation-view',
1947
+ panel: await panelInfo(pilotiq, req, relationViewRoute),
1948
+ resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
1949
+ relation: {
1950
+ name: M.name,
1951
+ label: M.getLabel(),
1952
+ labelSingular: M.getLabelSingular(),
1953
+ relationship: scope.relationship,
1954
+ icon: M.getIcon() ? serializeIcon(M.getIcon(), M.name) : undefined,
1955
+ relatedSlug: Related.getSlug(),
1956
+ },
1957
+ parent: {
1958
+ id: scope.recordId,
1959
+ title: deriveParentTitle(R, parentRecord),
1960
+ },
1961
+ mode: 'view',
1962
+ childId: scope.childId,
1963
+ basePath: base,
1964
+ layout: cfg.layout,
1965
+ schemaData,
1966
+ notifications: consumeFlashedNotifications(req),
1967
+ };
1968
+ }
1969
+ async function buildRelationEditData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode) {
1970
+ if (!Related.model) {
1971
+ throw new Error(`[Pilotiq] Cannot load child record for ${M.name}: Related Resource ${Related.name} has no static model.`);
1972
+ }
1973
+ const childPk = getPrimaryKey(Related.model);
1974
+ // IDOR check first — confirm the child actually belongs to the
1975
+ // parent under this relationship before doing anything else. Guards
1976
+ // against URL tampering swapping `:childId`.
1977
+ const belongs = await childBelongsToParent(R.model, parentRecord, scope.relationship, childPk, scope.childId);
1978
+ if (!belongs)
1979
+ return null;
1980
+ const child = await findRecord(Related, scope.childId, { user }).catch(() => undefined);
1981
+ if (!child)
1982
+ return null;
1983
+ if (!await safeManagerPolicy(M, 'canEdit', Related, user, parentRecord, child))
1984
+ return { ok: false, status: 403 };
1985
+ const cfg = pilotiq.getConfig();
1986
+ const base = cfg.path;
1987
+ const resourceBase = resourceBasePath(base, R);
1988
+ const editUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}/${scope.childId}/edit`;
1989
+ const managerCtx = {
1990
+ basePath: base,
1991
+ parentSlug: scope.slug,
1992
+ parentId: scope.recordId,
1993
+ relationship: scope.relationship,
1994
+ parentRecord,
1995
+ related: Related,
1996
+ mode,
1997
+ };
1998
+ const form = M.form(Form.make(), managerCtx);
1999
+ autoWireManagerForm(form, Related);
2000
+ const elements = [form];
2001
+ tagFormActions(elements, editUrl);
2002
+ // Prefill values: explicit prefill (re-render after 422) wins,
2003
+ // otherwise pipe the loaded child through Form's fill pipeline.
2004
+ if (scope.prefill?.values) {
2005
+ form.withValues(scope.prefill.values);
2006
+ if (scope.prefill.errors)
2007
+ form.withErrors(scope.prefill.errors);
2008
+ }
2009
+ else if (child != null) {
2010
+ const values = await applyFillPipeline(form, child);
2011
+ form.withValues(values);
2012
+ }
2013
+ const tabs = buildRelationTabs(R, scope.recordId, base, scope.relationship);
2014
+ if (tabs)
2015
+ elements.unshift(tabs);
2016
+ const breadcrumbs = relationEditBreadcrumbs(cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord), scope.childId, deriveParentTitle(Related, child, M));
2017
+ if (breadcrumbs)
2018
+ elements.unshift(breadcrumbs);
2019
+ const ctx = uploadCtx(userCtx({
2020
+ mode: 'edit',
2021
+ basePath: base,
2022
+ record: child,
2023
+ recordId: scope.childId,
2024
+ }, user), cfg);
2025
+ const relationEditRoute = { resource: R, recordId: scope.childId };
2026
+ const schemaData = await applyRoleHooks(pilotiq, user, 'relation-edit', await resolveSchema(elements, ctx), relationEditRoute);
2027
+ return {
2028
+ pageType: 'relation-edit',
2029
+ panel: await panelInfo(pilotiq, req, relationEditRoute),
2030
+ resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
2031
+ relation: {
2032
+ name: M.name,
2033
+ label: M.getLabel(),
2034
+ labelSingular: M.getLabelSingular(),
2035
+ relationship: scope.relationship,
2036
+ icon: M.getIcon() ? serializeIcon(M.getIcon(), M.name) : undefined,
2037
+ relatedSlug: Related.getSlug(),
2038
+ },
2039
+ parent: {
2040
+ id: scope.recordId,
2041
+ title: deriveParentTitle(R, parentRecord),
2042
+ },
2043
+ mode: 'edit',
2044
+ childId: scope.childId,
2045
+ basePath: base,
2046
+ layout: cfg.layout,
2047
+ schemaData,
2048
+ notifications: consumeFlashedNotifications(req),
2049
+ ...(scope.prefill?.errors ? { hasErrors: true } : {}),
2050
+ };
2051
+ }
2052
+ /**
2053
+ * Phase B — resolve a depth-2 chain, running every auth + IDOR layer:
2054
+ * Layer 0 — top-level Resource: cluster gate, R.canAccess.
2055
+ * Layer 1 — parent record: R.canEdit(parent) (Phase A gate to manage relations).
2056
+ * Layer 2 — first manager M1: relationship discovered, related resource discovered.
2057
+ * IDOR #1 — child1 (the leaf parent) must belong to parentRecord under chain[0].relationship.
2058
+ * Layer 3 — M1.canView(child1, parent) (Filament-style: must be allowed
2059
+ * to view the child to drill into its sub-relations).
2060
+ * Layer 4 — second manager M2 lookup; relation type read off Related1.model.
2061
+ *
2062
+ * The leaf manager's per-scope predicate (canViewAny / canCreate /
2063
+ * canView / canEdit) runs inside the per-scope builders below, since
2064
+ * each predicate has different arguments.
2065
+ */
2066
+ export async function resolveRelationChain(pilotiq, scope, user) {
2067
+ const cfg = pilotiq.getConfig();
2068
+ const R = cfg.resources.find(r => r.getSlug() === scope.slug);
2069
+ if (!R)
2070
+ return null;
2071
+ // Layer 0 — same gates as the depth-1 pipeline.
2072
+ if (R.cluster && !await safePolicy(() => R.cluster.canAccess(user)))
2073
+ return { ok: false, status: 403 };
2074
+ if (!await safePolicy(() => R.canAccess(user)))
2075
+ return { ok: false, status: 403 };
2076
+ if (!R.model) {
2077
+ throw new Error(`[Pilotiq] Resource "${R.name}" has nested relations but no static model. ` +
2078
+ `Set Resource.model = … or remove the manager.`);
2079
+ }
2080
+ const [step0, step1] = scope.chain;
2081
+ const parentRecord = await findRecord(R, step0.recordId, { user }).catch(() => undefined);
2082
+ if (!parentRecord)
2083
+ return null;
2084
+ // Layer 1 — parent record gate.
2085
+ if (!await safePolicy(() => R.canEdit(user, parentRecord)))
2086
+ return { ok: false, status: 403 };
2087
+ // Layer 2 — first manager M1.
2088
+ const M1 = findManager(R, step0.relationship);
2089
+ if (!M1)
2090
+ return null;
2091
+ const Related1 = findRelatedResource(M1, R, cfg);
2092
+ if (!Related1) {
2093
+ throw new Error(`[Pilotiq] RelationManager ${M1.name} on ${R.name} could not resolve its related Resource. ` +
2094
+ `Set static relatedResource on the manager, or ensure the parent's model declares relations[${JSON.stringify(M1.getRelationship())}].`);
2095
+ }
2096
+ if (!Related1.model) {
2097
+ throw new Error(`[Pilotiq] Related Resource ${Related1.name} has no static model — ` +
2098
+ `cannot resolve nested manager chain through it.`);
2099
+ }
2100
+ const child1Mode = normalizeRelationMode(getRelationType(R.model, step0.relationship));
2101
+ // IDOR #1 — confirm the leaf parent (`step1.recordId`) actually
2102
+ // belongs to the top parent under the first relationship key.
2103
+ const child1Pk = getPrimaryKey(Related1.model);
2104
+ const belongs1 = await childBelongsToParent(R.model, parentRecord, step0.relationship, child1Pk, step1.recordId);
2105
+ if (!belongs1)
2106
+ return null;
2107
+ const child1 = await findRecord(Related1, step1.recordId, { user }).catch(() => undefined);
2108
+ if (!child1)
2109
+ return null;
2110
+ // Layer 3 — M1.canView(child1, parent) gate. Filament-style: viewing
2111
+ // the child is the prerequisite for entering its nested manager strip.
2112
+ if (!await safeManagerPolicy(M1, 'canView', Related1, user, parentRecord, child1))
2113
+ return { ok: false, status: 403 };
2114
+ // Layer 4 — second manager M2 declared under M1.relations().
2115
+ const M2 = M1.relations().find(N => {
2116
+ try {
2117
+ return N.getRelationship() === step1.relationship;
2118
+ }
2119
+ catch {
2120
+ return false;
2121
+ }
2122
+ });
2123
+ if (!M2)
2124
+ return null;
2125
+ const Related2 = findRelatedResource(M2, Related1, cfg);
2126
+ const child2Mode = normalizeRelationMode(getRelationType(Related1.model, step1.relationship));
2127
+ return { R, parentRecord, M1, Related1, child1, child1Mode, M2, Related2, child2Mode };
2128
+ }
2129
+ /**
2130
+ * Phase B dispatcher — splits the four nested scopes onto their builders
2131
+ * after the shared chain walk. Mirrors the depth-1 `relationManagerData`
2132
+ * function shape.
2133
+ */
2134
+ async function nestedRelationManagerData(pilotiq, scope, req) {
2135
+ const user = await pilotiq.resolveUser(req);
2136
+ const resolved = await resolveRelationChain(pilotiq, scope, user);
2137
+ if (resolved === null)
2138
+ return null;
2139
+ if ('ok' in resolved)
2140
+ return resolved;
2141
+ // For create / view / edit we strictly need a registered Related2 so
2142
+ // we can load the leaf record + auto-wire the form save.
2143
+ const needRelated2 = scope.kind !== 'nested-relation-list';
2144
+ if (needRelated2 && !resolved.Related2) {
2145
+ throw new Error(`[Pilotiq] Nested RelationManager ${resolved.M2.name} under ${resolved.M1.name} ` +
2146
+ `on ${resolved.R.name} could not resolve its related Resource. ` +
2147
+ `Set static relatedResource on the manager, or ensure the parent's model declares ` +
2148
+ `relations[${JSON.stringify(resolved.M2.getRelationship())}].`);
2149
+ }
2150
+ switch (scope.kind) {
2151
+ case 'nested-relation-list':
2152
+ return buildNestedRelationListData(pilotiq, scope, resolved, req, user);
2153
+ case 'nested-relation-create':
2154
+ return buildNestedRelationCreateData(pilotiq, scope, resolved, req, user);
2155
+ case 'nested-relation-view':
2156
+ return buildNestedRelationViewData(pilotiq, scope, resolved, req, user);
2157
+ case 'nested-relation-edit':
2158
+ return buildNestedRelationEditData(pilotiq, scope, resolved, req, user);
2159
+ }
2160
+ }
2161
+ /** Phase B — build the manager context for a nested leaf manager. The
2162
+ * parent here is `child1` (the chain's leaf parent record); the URL
2163
+ * prefix comes from `scope.chain[0]` via `Action.relation*` factories
2164
+ * reading `ctx.chain`. */
2165
+ function nestedManagerCtx(base, scope, resolved) {
2166
+ const [step0, step1] = scope.chain;
2167
+ return {
2168
+ basePath: base,
2169
+ parentSlug: resolved.R.getSlug(),
2170
+ parentId: step1.recordId, // immediate parent = child1's id
2171
+ relationship: step1.relationship, // leaf manager's relationship
2172
+ parentRecord: resolved.child1, // immediate parent record = child1
2173
+ related: resolved.Related2,
2174
+ mode: resolved.child2Mode,
2175
+ chain: [{
2176
+ slug: resolved.R.getSlug(),
2177
+ recordId: step0.recordId,
2178
+ relationship: step0.relationship,
2179
+ }],
2180
+ };
2181
+ }
2182
+ /** Phase B — assemble the response shape that mirrors the depth-1
2183
+ * builders but adds a `chain` array so renderers can build breadcrumbs
2184
+ * and back-links without re-deriving them. */
2185
+ function nestedResponseEnvelope(pageType, pilotiq, base, scope, resolved, req) {
2186
+ const { R, M1, Related1, child1, M2, Related2 } = resolved;
2187
+ const [step0, step1] = scope.chain;
2188
+ const parentChildTitle = deriveParentTitle(Related1, child1, M1);
2189
+ return {
2190
+ pageType,
2191
+ resource: { name: R.name, label: R.labelSingular, slug: R.getSlug(), icon: serializeIcon(R.icon, R.name) },
2192
+ parentRelation: {
2193
+ name: M1.name,
2194
+ relationship: step0.relationship,
2195
+ label: M1.getLabel(),
2196
+ relatedSlug: Related1.getSlug(),
2197
+ },
2198
+ parentChild: {
2199
+ id: step1.recordId,
2200
+ title: parentChildTitle,
2201
+ },
2202
+ relation: {
2203
+ name: M2.name,
2204
+ relationship: step1.relationship,
2205
+ label: M2.getLabel(),
2206
+ labelSingular: M2.getLabelSingular(),
2207
+ icon: M2.getIcon() ? serializeIcon(M2.getIcon(), M2.name) : undefined,
2208
+ relatedSlug: Related2?.getSlug(),
2209
+ },
2210
+ parent: {
2211
+ // Top-of-chain record — same shape the depth-1 builders ship as
2212
+ // `parent` so renderers can reuse the back-to-resource link.
2213
+ id: step0.recordId,
2214
+ title: deriveParentTitle(R, resolved.parentRecord),
2215
+ },
2216
+ basePath: base,
2217
+ layout: pilotiq.getConfig().layout,
2218
+ notifications: consumeFlashedNotifications(req),
2219
+ };
2220
+ }
2221
+ async function buildNestedRelationListData(pilotiq, scope, resolved, req, user) {
2222
+ const { Related1, child1, M2, Related2 } = resolved;
2223
+ if (!await safeManagerPolicy(M2, 'canViewAny', Related2, user, child1))
2224
+ return { ok: false, status: 403 };
2225
+ const cfg = pilotiq.getConfig();
2226
+ const base = cfg.path;
2227
+ const [step0, step1] = scope.chain;
2228
+ const resourceBase = resourceBasePath(base, resolved.R);
2229
+ const listUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}`;
2230
+ const managerCtx = nestedManagerCtx(base, scope, resolved);
2231
+ const table = M2.table(Table.make(), managerCtx);
2232
+ if (Related1.model) {
2233
+ autoWireManagerTable(table, Related1.model, child1, step1.relationship);
2234
+ }
2235
+ injectManagerTrashedFilter(table, Related2);
2236
+ const ctx = uploadCtx(userCtx({
2237
+ mode: 'table',
2238
+ basePath: base,
2239
+ record: child1,
2240
+ }, user), cfg);
2241
+ const elements = [table];
2242
+ tagActionDispatch(elements, listUrl);
2243
+ await loadTableRecords(elements, scope.query ?? {}, listUrl, user);
2244
+ const tabs = buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship);
2245
+ if (tabs)
2246
+ elements.unshift(tabs);
2247
+ const breadcrumbs = nestedRelationListBreadcrumbs(cfg, resolved.R, resolved.M1, M2, scope.chain[0], deriveParentTitle(resolved.R, resolved.parentRecord), scope.chain[1].recordId, deriveParentTitle(Related1, child1, resolved.M1));
2248
+ if (breadcrumbs)
2249
+ elements.unshift(breadcrumbs);
2250
+ const nestedListRoute = { resource: resolved.R, recordId: scope.chain[1].recordId };
2251
+ const schemaData = await applyRoleHooks(pilotiq, user, 'relation-list', await resolveSchema(elements, ctx), nestedListRoute);
2252
+ return {
2253
+ ...nestedResponseEnvelope('nested-relation-list', pilotiq, base, scope, resolved, req),
2254
+ panel: await panelInfo(pilotiq, req, nestedListRoute),
2255
+ schemaData,
2256
+ };
2257
+ }
2258
+ async function buildNestedRelationCreateData(pilotiq, scope, resolved, req, user) {
2259
+ const { child1, M2, Related2 } = resolved;
2260
+ if (!await safeManagerPolicy(M2, 'canCreate', Related2, user, child1))
2261
+ return { ok: false, status: 403 };
2262
+ const cfg = pilotiq.getConfig();
2263
+ const base = cfg.path;
2264
+ const [step0, step1] = scope.chain;
2265
+ const resourceBase = resourceBasePath(base, resolved.R);
2266
+ const createUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}/create`;
2267
+ const managerCtx = nestedManagerCtx(base, scope, resolved);
2268
+ const form = M2.form(Form.make(), managerCtx);
2269
+ if (Related2?.model)
2270
+ autoWireManagerForm(form, Related2);
2271
+ const elements = [form];
2272
+ tagFormActions(elements, createUrl);
2273
+ if (scope.prefill) {
2274
+ if (scope.prefill.values)
2275
+ form.withValues(scope.prefill.values);
2276
+ if (scope.prefill.errors)
2277
+ form.withErrors(scope.prefill.errors);
2278
+ }
2279
+ const tabs = buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship);
2280
+ if (tabs)
2281
+ elements.unshift(tabs);
2282
+ const breadcrumbs = nestedRelationCreateBreadcrumbs(cfg, resolved.R, resolved.M1, M2, scope.chain[0], deriveParentTitle(resolved.R, resolved.parentRecord), scope.chain[1].recordId, deriveParentTitle(resolved.Related1, child1, resolved.M1));
2283
+ if (breadcrumbs)
2284
+ elements.unshift(breadcrumbs);
2285
+ const ctx = uploadCtx(userCtx({
2286
+ mode: 'create',
2287
+ basePath: base,
2288
+ record: child1,
2289
+ }, user), cfg);
2290
+ const nestedCreateRoute = { resource: resolved.R, recordId: scope.chain[1].recordId };
2291
+ const schemaData = await applyRoleHooks(pilotiq, user, 'relation-create', await resolveSchema(elements, ctx), nestedCreateRoute);
2292
+ return {
2293
+ ...nestedResponseEnvelope('nested-relation-create', pilotiq, base, scope, resolved, req),
2294
+ panel: await panelInfo(pilotiq, req, nestedCreateRoute),
2295
+ mode: 'create',
2296
+ schemaData,
2297
+ ...(scope.prefill?.errors ? { hasErrors: true } : {}),
2298
+ };
2299
+ }
2300
+ async function buildNestedRelationViewData(pilotiq, scope, resolved, req, user) {
2301
+ const { Related1, child1, M2, Related2 } = resolved;
2302
+ if (!Related2?.model) {
2303
+ throw new Error(`[Pilotiq] Cannot load child record for nested manager ${M2.name}: ` +
2304
+ `Related Resource ${Related2?.name ?? '(none)'} has no static model.`);
2305
+ }
2306
+ const [, step1] = scope.chain;
2307
+ const child2Pk = getPrimaryKey(Related2.model);
2308
+ const belongs2 = await childBelongsToParent(Related1.model, child1, step1.relationship, child2Pk, scope.childId);
2309
+ if (!belongs2)
2310
+ return null;
2311
+ const child2 = await findRecord(Related2, scope.childId, { user }).catch(() => undefined);
2312
+ if (!child2)
2313
+ return null;
2314
+ if (!await safeManagerPolicy(M2, 'canView', Related2, user, child1, child2))
2315
+ return { ok: false, status: 403 };
2316
+ const cfg = pilotiq.getConfig();
2317
+ const base = cfg.path;
2318
+ const elements = M2.detail(child2, child1);
2319
+ const tabs = buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship);
2320
+ if (tabs)
2321
+ elements.unshift(tabs);
2322
+ const breadcrumbs = nestedRelationViewBreadcrumbs(cfg, resolved.R, resolved.M1, M2, scope.chain[0], deriveParentTitle(resolved.R, resolved.parentRecord), scope.chain[1].recordId, deriveParentTitle(Related1, child1, resolved.M1), deriveParentTitle(Related2, child2, M2));
2323
+ if (breadcrumbs)
2324
+ elements.unshift(breadcrumbs);
2325
+ const ctx = uploadCtx(userCtx({
2326
+ mode: 'view',
2327
+ basePath: base,
2328
+ record: child2,
2329
+ recordId: scope.childId,
2330
+ }, user), cfg);
2331
+ const nestedViewRoute = { resource: resolved.R, recordId: scope.childId };
2332
+ const schemaData = await applyRoleHooks(pilotiq, user, 'relation-view', await resolveSchema(elements, ctx), nestedViewRoute);
2333
+ return {
2334
+ ...nestedResponseEnvelope('nested-relation-view', pilotiq, base, scope, resolved, req),
2335
+ panel: await panelInfo(pilotiq, req, nestedViewRoute),
2336
+ mode: 'view',
2337
+ childId: scope.childId,
2338
+ schemaData,
2339
+ };
2340
+ }
2341
+ async function buildNestedRelationEditData(pilotiq, scope, resolved, req, user) {
2342
+ const { Related1, child1, M2, Related2 } = resolved;
2343
+ if (!Related2?.model) {
2344
+ throw new Error(`[Pilotiq] Cannot load child record for nested manager ${M2.name}: ` +
2345
+ `Related Resource ${Related2?.name ?? '(none)'} has no static model.`);
2346
+ }
2347
+ const [step0, step1] = scope.chain;
2348
+ const child2Pk = getPrimaryKey(Related2.model);
2349
+ const belongs2 = await childBelongsToParent(Related1.model, child1, step1.relationship, child2Pk, scope.childId);
2350
+ if (!belongs2)
2351
+ return null;
2352
+ const child2 = await findRecord(Related2, scope.childId, { user }).catch(() => undefined);
2353
+ if (!child2)
2354
+ return null;
2355
+ if (!await safeManagerPolicy(M2, 'canEdit', Related2, user, child1, child2))
2356
+ return { ok: false, status: 403 };
2357
+ const cfg = pilotiq.getConfig();
2358
+ const base = cfg.path;
2359
+ const resourceBase = resourceBasePath(base, resolved.R);
2360
+ const editUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}/${scope.childId}/edit`;
2361
+ const managerCtx = nestedManagerCtx(base, scope, resolved);
2362
+ const form = M2.form(Form.make(), managerCtx);
2363
+ autoWireManagerForm(form, Related2);
2364
+ const elements = [form];
2365
+ tagFormActions(elements, editUrl);
2366
+ if (scope.prefill?.values) {
2367
+ form.withValues(scope.prefill.values);
2368
+ if (scope.prefill.errors)
2369
+ form.withErrors(scope.prefill.errors);
2370
+ }
2371
+ else if (child2 != null) {
2372
+ const values = await applyFillPipeline(form, child2);
2373
+ form.withValues(values);
2374
+ }
2375
+ const tabs = buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship);
2376
+ if (tabs)
2377
+ elements.unshift(tabs);
2378
+ const breadcrumbs = nestedRelationEditBreadcrumbs(cfg, resolved.R, resolved.M1, M2, scope.chain[0], deriveParentTitle(resolved.R, resolved.parentRecord), scope.chain[1].recordId, deriveParentTitle(Related1, child1, resolved.M1), scope.childId, deriveParentTitle(Related2, child2, M2));
2379
+ if (breadcrumbs)
2380
+ elements.unshift(breadcrumbs);
2381
+ const ctx = uploadCtx(userCtx({
2382
+ mode: 'edit',
2383
+ basePath: base,
2384
+ record: child2,
2385
+ recordId: scope.childId,
2386
+ }, user), cfg);
2387
+ const nestedEditRoute = { resource: resolved.R, recordId: scope.childId };
2388
+ const schemaData = await applyRoleHooks(pilotiq, user, 'relation-edit', await resolveSchema(elements, ctx), nestedEditRoute);
2389
+ return {
2390
+ ...nestedResponseEnvelope('nested-relation-edit', pilotiq, base, scope, resolved, req),
2391
+ panel: await panelInfo(pilotiq, req, nestedEditRoute),
2392
+ mode: 'edit',
2393
+ childId: scope.childId,
2394
+ schemaData,
2395
+ ...(scope.prefill?.errors ? { hasErrors: true } : {}),
2396
+ };
2397
+ }
2398
+ /**
2399
+ * Phase B — build a `RelationTabs` strip scoped to a parent's nested
2400
+ * children (e.g. tabs for `replies / reactions` listed under a single
2401
+ * comment). The strip's "back" tab points to the depth-2 view page for
2402
+ * the leaf parent itself, then one tab per sibling nested manager.
2403
+ *
2404
+ * Only emitted when the depth-1 manager declares `M.relations()` —
2405
+ * absent that, callers skip the prepend so single-manager surfaces stay
2406
+ * clean. `activeKey` accepts the literal `'__view'` for the leaf
2407
+ * parent's view tab, or any sibling manager's relationship key.
2408
+ */
2409
+ function buildNestedRelationTabs(R, M, basePath, step0, child1Id, activeKey) {
2410
+ const siblings = M.relations();
2411
+ if (siblings.length === 0)
2412
+ return undefined;
2413
+ const resourceBase = resourceBasePath(basePath, R);
2414
+ const parentBase = `${resourceBase}/${step0.recordId}/${step0.relationship}`;
2415
+ const tabs = [];
2416
+ // Back-link: depth-2 view page for the leaf parent record. Acts as
2417
+ // the "View" tab in the same way `__view` does on depth-1 strips.
2418
+ tabs.push(relationTab({
2419
+ key: '__view',
2420
+ label: M.getLabelSingular(),
2421
+ url: `${parentBase}/${child1Id}`,
2422
+ active: activeKey === '__view',
2423
+ icon: M.getIcon(),
2424
+ iconOwner: M.name,
2425
+ }));
2426
+ for (const N of siblings) {
2427
+ let nestedRel = '';
2428
+ try {
2429
+ nestedRel = N.getRelationship();
2430
+ }
2431
+ catch {
2432
+ continue;
2433
+ }
2434
+ const icon = N.getIcon();
2435
+ tabs.push(relationTab({
2436
+ key: nestedRel,
2437
+ label: N.getLabel(),
2438
+ url: `${parentBase}/${child1Id}/${nestedRel}`,
2439
+ active: activeKey === nestedRel,
2440
+ ...(icon !== undefined ? { icon, iconOwner: N.name } : {}),
2441
+ }));
2442
+ }
2443
+ return RelationTabs.make(tabs);
2444
+ }
2445
+ /**
2446
+ * Plan #11 — build the `RelationTabs` strip for a parent record. The
2447
+ * strip surfaces the per-record sub-navigation: View, Edit, plus one
2448
+ * tab per `R.relations()` manager. `activeKey` selects which tab the
2449
+ * renderer highlights — `'__view'` / `'__edit'` for the parent tabs,
2450
+ * the manager's relationship key for a manager tab.
2451
+ *
2452
+ * Sub-nav follow-up (2026-05-03 cont'd) — emit BOTH `__view` and
2453
+ * `__edit` as sibling tabs (Filament-style record sub-navigation)
2454
+ * instead of one parent tab whose label depends on mode. Tabs are
2455
+ * dropped when the corresponding page role isn't registered (a
2456
+ * Resource overriding `pages()` to omit `view` or `edit` shouldn't
2457
+ * surface a tab that 404s).
2458
+ *
2459
+ * Returns `undefined` when the resource has no relation managers — the
2460
+ * caller can then skip the prepend entirely so resources without
2461
+ * relations stay shape-compatible with their existing schemaData.
2462
+ * (View+Edit sub-nav alone isn't worth a tab strip; users navigate
2463
+ * those via headerActions or the back link.)
2464
+ */
2465
+ function buildRelationTabs(R, recordId, basePath, activeKey) {
2466
+ const managers = R.relations();
2467
+ if (managers.length === 0)
2468
+ return undefined;
2469
+ const resourceBase = resourceBasePath(basePath, R);
2470
+ const pages = R.resolvePages();
2471
+ const tabs = [];
2472
+ // View tab — only when the resource has a ViewPage registered.
2473
+ // Defaults always include one; users who pruned ViewPage in their
2474
+ // `static pages()` override get no broken link.
2475
+ if (pages.view) {
2476
+ tabs.push(relationTab({
2477
+ key: '__view',
2478
+ label: 'View',
2479
+ url: `${resourceBase}/${recordId}`,
2480
+ active: activeKey === '__view',
2481
+ icon: R.icon,
2482
+ iconOwner: R.name,
2483
+ }));
2484
+ }
2485
+ // Edit tab — same defensive check.
2486
+ if (pages.edit) {
2487
+ tabs.push(relationTab({
2488
+ key: '__edit',
2489
+ label: 'Edit',
2490
+ url: `${resourceBase}/${recordId}/edit`,
2491
+ active: activeKey === '__edit',
2492
+ // Re-use the resource icon so when ViewPage is pruned, Edit
2493
+ // still carries the visual identity. When both are present, the
2494
+ // icon repeats — acceptable; the labels disambiguate.
2495
+ icon: R.icon,
2496
+ iconOwner: R.name,
2497
+ }));
2498
+ }
2499
+ for (const M of managers) {
2500
+ let rel = '';
2501
+ try {
2502
+ rel = M.getRelationship();
2503
+ }
2504
+ catch {
2505
+ continue;
2506
+ }
2507
+ const icon = M.getIcon();
2508
+ tabs.push(relationTab({
2509
+ key: rel,
2510
+ label: M.getLabel(),
2511
+ url: `${resourceBase}/${recordId}/${rel}`,
2512
+ active: activeKey === rel,
2513
+ ...(icon !== undefined ? { icon, iconOwner: M.name } : {}),
2514
+ }));
2515
+ }
2516
+ return RelationTabs.make(tabs);
2517
+ }
2518
+ /** Pull a human-readable title off a parent record for breadcrumb /
2519
+ * page-title use. Falls back through `recordTitleAttribute` →
2520
+ * `name` → `title` → primary key value → 'Record'. */
2521
+ function deriveParentTitle(R, record, manager) {
2522
+ const r = record;
2523
+ // Manager-scoped child rows prefer the manager's `recordTitleAttribute`
2524
+ // when set — the manager owns its presentation surface, and the related
2525
+ // Resource may not opt into the same column (e.g. nested-only Resources
2526
+ // that exist purely to back a manager).
2527
+ const managerAttr = manager?.recordTitleAttribute;
2528
+ if (managerAttr && r[managerAttr] != null)
2529
+ return String(r[managerAttr]);
2530
+ const attr = R.recordTitleAttribute;
2531
+ if (attr && r[attr] != null)
2532
+ return String(r[attr]);
2533
+ if (r['name'] != null)
2534
+ return String(r['name']);
2535
+ if (r['title'] != null)
2536
+ return String(r['title']);
2537
+ if (R.model) {
2538
+ const pk = getPrimaryKey(R.model);
2539
+ if (r[pk] != null)
2540
+ return String(r[pk]);
2541
+ }
2542
+ return 'Record';
2543
+ }
2544
+ // ─── Phase C breadcrumb builders ─────────────────────────────
2545
+ //
2546
+ // Server-resolved chain rendered above any other top-of-page chrome
2547
+ // (e.g. RelationTabs). The trailing item is always the current page,
2548
+ // emitted without a `url` so the renderer can paint it as plain text
2549
+ // + `aria-current="page"`. All earlier items link to their canonical
2550
+ // URL — clusters route through `clusterBasePath`, resources through
2551
+ // `resourceBasePath`, etc., so a clustered resource resolves to
2552
+ // `Home / Cluster / Resource / …` instead of skipping the cluster
2553
+ // rung.
2554
+ function homeBreadcrumb(cfg) {
2555
+ return {
2556
+ label: cfg.branding?.title ?? cfg.name ?? 'Home',
2557
+ url: cfg.path,
2558
+ };
2559
+ }
2560
+ function clusterBreadcrumb(cfg, child) {
2561
+ if (!child.cluster)
2562
+ return undefined;
2563
+ return {
2564
+ label: child.cluster.label,
2565
+ url: clusterBasePath(cfg.path, child.cluster),
2566
+ };
2567
+ }
2568
+ function buildBreadcrumbs(items) {
2569
+ // A single "Home" rung carries no information beyond the dashboard
2570
+ // link the layout already exposes — drop it. Every other length is
2571
+ // worth rendering.
2572
+ if (items.length < 2)
2573
+ return undefined;
2574
+ return Breadcrumbs.make(items);
2575
+ }
2576
+ function resourceListBreadcrumbs(cfg, R) {
2577
+ const items = [homeBreadcrumb(cfg)];
2578
+ const cluster = clusterBreadcrumb(cfg, R);
2579
+ if (cluster)
2580
+ items.push(cluster);
2581
+ items.push({ label: R.getBreadcrumb() });
2582
+ return buildBreadcrumbs(items);
2583
+ }
2584
+ function resourceCreateBreadcrumbs(cfg, R) {
2585
+ const items = [homeBreadcrumb(cfg)];
2586
+ const cluster = clusterBreadcrumb(cfg, R);
2587
+ if (cluster)
2588
+ items.push(cluster);
2589
+ items.push({ label: R.getBreadcrumb(), url: resourceBasePath(cfg.path, R) });
2590
+ items.push({ label: 'Create' });
2591
+ return buildBreadcrumbs(items);
2592
+ }
2593
+ function resourceViewBreadcrumbs(cfg, R, recordTitle) {
2594
+ const items = [homeBreadcrumb(cfg)];
2595
+ const cluster = clusterBreadcrumb(cfg, R);
2596
+ if (cluster)
2597
+ items.push(cluster);
2598
+ items.push({ label: R.getBreadcrumb(), url: resourceBasePath(cfg.path, R) });
2599
+ items.push({ label: recordTitle });
2600
+ return buildBreadcrumbs(items);
2601
+ }
2602
+ function resourceEditBreadcrumbs(cfg, R, recordId, recordTitle) {
2603
+ const items = [homeBreadcrumb(cfg)];
2604
+ const cluster = clusterBreadcrumb(cfg, R);
2605
+ if (cluster)
2606
+ items.push(cluster);
2607
+ const resourceBase = resourceBasePath(cfg.path, R);
2608
+ items.push({ label: R.getBreadcrumb(), url: resourceBase });
2609
+ // Link the record title to the View page when registered — falls
2610
+ // back to plain text so users who pruned ViewPage don't hit a 404.
2611
+ const hasView = R.resolvePages().view !== undefined;
2612
+ items.push(hasView
2613
+ ? { label: recordTitle, url: `${resourceBase}/${recordId}` }
2614
+ : { label: recordTitle });
2615
+ items.push({ label: 'Edit' });
2616
+ return buildBreadcrumbs(items);
2617
+ }
2618
+ function globalBreadcrumbs(cfg, G) {
2619
+ // Globals don't have a list page — `Home > <Global Label>` is the
2620
+ // shortest meaningful chain. Edit and View collapse to the same
2621
+ // breadcrumb (both render the singleton).
2622
+ const items = [homeBreadcrumb(cfg)];
2623
+ const cluster = clusterBreadcrumb(cfg, G);
2624
+ if (cluster)
2625
+ items.push(cluster);
2626
+ items.push({ label: G.label });
2627
+ return buildBreadcrumbs(items);
2628
+ }
2629
+ function customPageBreadcrumbs(cfg, P) {
2630
+ const items = [homeBreadcrumb(cfg)];
2631
+ const cluster = clusterBreadcrumb(cfg, P);
2632
+ if (cluster)
2633
+ items.push(cluster);
2634
+ items.push({ label: P.getLabel() });
2635
+ return buildBreadcrumbs(items);
2636
+ }
2637
+ /** Common "Home / cluster? / Resource / parent record" prefix used by
2638
+ * every relation-* / nested-relation-* breadcrumb. The parent record
2639
+ * links to its View page when registered; the resource list is the
2640
+ * fallback so users still have a back-link out of the relation chain. */
2641
+ function relationBreadcrumbPrefix(cfg, R, parentId, parentTitle) {
2642
+ const items = [homeBreadcrumb(cfg)];
2643
+ const cluster = clusterBreadcrumb(cfg, R);
2644
+ if (cluster)
2645
+ items.push(cluster);
2646
+ const resourceBase = resourceBasePath(cfg.path, R);
2647
+ items.push({ label: R.getBreadcrumb(), url: resourceBase });
2648
+ const hasView = R.resolvePages().view !== undefined;
2649
+ items.push(hasView
2650
+ ? { label: parentTitle, url: `${resourceBase}/${parentId}` }
2651
+ : { label: parentTitle });
2652
+ return items;
2653
+ }
2654
+ function relationListBreadcrumbs(cfg, R, M, parentId, parentTitle) {
2655
+ const items = relationBreadcrumbPrefix(cfg, R, parentId, parentTitle);
2656
+ items.push({ label: M.getLabel() });
2657
+ return buildBreadcrumbs(items);
2658
+ }
2659
+ function relationCreateBreadcrumbs(cfg, R, M, parentId, parentTitle) {
2660
+ const items = relationBreadcrumbPrefix(cfg, R, parentId, parentTitle);
2661
+ const relList = `${resourceBasePath(cfg.path, R)}/${parentId}/${M.getRelationship()}`;
2662
+ items.push({ label: M.getLabel(), url: relList });
2663
+ items.push({ label: 'Create' });
2664
+ return buildBreadcrumbs(items);
2665
+ }
2666
+ function relationViewBreadcrumbs(cfg, R, M, parentId, parentTitle, childTitle) {
2667
+ const items = relationBreadcrumbPrefix(cfg, R, parentId, parentTitle);
2668
+ const relList = `${resourceBasePath(cfg.path, R)}/${parentId}/${M.getRelationship()}`;
2669
+ items.push({ label: M.getLabel(), url: relList });
2670
+ items.push({ label: childTitle });
2671
+ return buildBreadcrumbs(items);
2672
+ }
2673
+ function relationEditBreadcrumbs(cfg, R, M, parentId, parentTitle, childId, childTitle) {
2674
+ const items = relationBreadcrumbPrefix(cfg, R, parentId, parentTitle);
2675
+ const relBase = `${resourceBasePath(cfg.path, R)}/${parentId}/${M.getRelationship()}`;
2676
+ items.push({ label: M.getLabel(), url: relBase });
2677
+ // Phase A always mounts the relation-view page per (R, M), so the
2678
+ // child title can always link back to it.
2679
+ items.push({ label: childTitle, url: `${relBase}/${childId}` });
2680
+ items.push({ label: 'Edit' });
2681
+ return buildBreadcrumbs(items);
2682
+ }
2683
+ /** Phase B — depth-2 prefix shared by every nested-relation-* role.
2684
+ * Returns "Home / cluster? / Resource / parent / M1 / child1". */
2685
+ function nestedRelationBreadcrumbPrefix(cfg, R, M1, step0, parentTitle, child1Id, child1Title) {
2686
+ const items = relationBreadcrumbPrefix(cfg, R, step0.recordId, parentTitle);
2687
+ const rel1Base = `${resourceBasePath(cfg.path, R)}/${step0.recordId}/${step0.relationship}`;
2688
+ items.push({ label: M1.getLabel(), url: rel1Base });
2689
+ // Phase A relation-view always mounted, so child1 always links.
2690
+ items.push({ label: child1Title, url: `${rel1Base}/${child1Id}` });
2691
+ return items;
2692
+ }
2693
+ function nestedRelationListBreadcrumbs(cfg, R, M1, M2, step0, parentTitle, child1Id, child1Title) {
2694
+ const items = nestedRelationBreadcrumbPrefix(cfg, R, M1, step0, parentTitle, child1Id, child1Title);
2695
+ items.push({ label: M2.getLabel() });
2696
+ return buildBreadcrumbs(items);
2697
+ }
2698
+ function nestedRelationCreateBreadcrumbs(cfg, R, M1, M2, step0, parentTitle, child1Id, child1Title) {
2699
+ const items = nestedRelationBreadcrumbPrefix(cfg, R, M1, step0, parentTitle, child1Id, child1Title);
2700
+ const rel2Base = `${resourceBasePath(cfg.path, R)}/${step0.recordId}/${step0.relationship}/${child1Id}/${M2.getRelationship()}`;
2701
+ items.push({ label: M2.getLabel(), url: rel2Base });
2702
+ items.push({ label: 'Create' });
2703
+ return buildBreadcrumbs(items);
2704
+ }
2705
+ function nestedRelationViewBreadcrumbs(cfg, R, M1, M2, step0, parentTitle, child1Id, child1Title, child2Title) {
2706
+ const items = nestedRelationBreadcrumbPrefix(cfg, R, M1, step0, parentTitle, child1Id, child1Title);
2707
+ const rel2Base = `${resourceBasePath(cfg.path, R)}/${step0.recordId}/${step0.relationship}/${child1Id}/${M2.getRelationship()}`;
2708
+ items.push({ label: M2.getLabel(), url: rel2Base });
2709
+ items.push({ label: child2Title });
2710
+ return buildBreadcrumbs(items);
2711
+ }
2712
+ function nestedRelationEditBreadcrumbs(cfg, R, M1, M2, step0, parentTitle, child1Id, child1Title, child2Id, child2Title) {
2713
+ const items = nestedRelationBreadcrumbPrefix(cfg, R, M1, step0, parentTitle, child1Id, child1Title);
2714
+ const rel2Base = `${resourceBasePath(cfg.path, R)}/${step0.recordId}/${step0.relationship}/${child1Id}/${M2.getRelationship()}`;
2715
+ items.push({ label: M2.getLabel(), url: rel2Base });
2716
+ items.push({ label: child2Title, url: `${rel2Base}/${child2Id}` });
2717
+ items.push({ label: 'Edit' });
2718
+ return buildBreadcrumbs(items);
2719
+ }
2720
+ /**
2721
+ * Plan #5 — handle a partial-resolve roundtrip from a `live()` field.
2722
+ *
2723
+ * Locates the page's schema, finds the targeted form by `formId`, runs
2724
+ * `applyStateUpdate` to apply the changed value + run
2725
+ * `afterStateUpdated`, then re-resolves the form's children with the
2726
+ * mutated values + bound `$get / $set` so dependent options /
2727
+ * conditional visibility re-evaluate. Returns the resolved FormMeta the
2728
+ * client uses to replace its rendered form.
2729
+ *
2730
+ * Returns `null` when the route prefix doesn't resolve to a real
2731
+ * resource/global/page — the route handler turns this into a 404. The
2732
+ * inner `{ status: 422 }` failure is for "form found but `changed`
2733
+ * field doesn't exist on it" — also a client-side bug.
2734
+ */
2735
+ export async function formStateData(pilotiq, scope, body, req) {
2736
+ const cfg = pilotiq.getConfig();
2737
+ const user = await pilotiq.resolveUser(req);
2738
+ let PageClass;
2739
+ let mode;
2740
+ let record = undefined;
2741
+ let recordId;
2742
+ let baseCtxExtras = {};
2743
+ if (scope.kind === 'resource-create' || scope.kind === 'resource-edit') {
2744
+ const R = cfg.resources.find(r => r.getSlug() === scope.slug);
2745
+ if (!R)
2746
+ return null;
2747
+ const pages = R.resolvePages();
2748
+ if (scope.kind === 'resource-create') {
2749
+ if (!pages.create)
2750
+ return null;
2751
+ PageClass = pages.create;
2752
+ mode = 'create';
2753
+ }
2754
+ else {
2755
+ if (!pages.edit)
2756
+ return null;
2757
+ PageClass = pages.edit;
2758
+ mode = 'edit';
2759
+ recordId = scope.recordId;
2760
+ baseCtxExtras = { recordId };
2761
+ if (R.model) {
2762
+ try {
2763
+ record = await findRecord(R, scope.recordId, { user });
2764
+ }
2765
+ catch { /* ignore */ }
2766
+ }
2767
+ else if (recordId) {
2768
+ record = { id: recordId };
2769
+ }
2770
+ }
2771
+ }
2772
+ else if (scope.kind === 'global-edit') {
2773
+ const G = cfg.globals.find(g => g.getSlug() === scope.slug);
2774
+ if (!G)
2775
+ return null;
2776
+ const pages = G.resolvePages();
2777
+ if (!pages.edit)
2778
+ return null;
2779
+ PageClass = pages.edit;
2780
+ mode = 'edit';
2781
+ }
2782
+ else {
2783
+ const P = cfg.pages.find(p => p.getSlug() === scope.pageSlug);
2784
+ if (!P)
2785
+ return null;
2786
+ PageClass = P;
2787
+ // Custom pages don't have a record/edit-mode concept — pass mode
2788
+ // 'edit' so resolveSchema treats fields as form inputs (not table
2789
+ // cells / view-mode read-only).
2790
+ mode = 'edit';
2791
+ }
2792
+ if (!PageClass)
2793
+ return null;
2794
+ const baseCtx = uploadCtx(userCtx({ mode, basePath: cfg.path, ...baseCtxExtras }, user), cfg);
2795
+ const elements = await callPageSchema(PageClass, baseCtx);
2796
+ const form = selectFormById(findForms(elements), body.formId);
2797
+ if (!form)
2798
+ return { ok: false, status: 404, error: `Form "${body.formId}" not found on page` };
2799
+ const update = await applyStateUpdate(form, body.values, body.changed, {
2800
+ ...(record !== undefined ? { record } : {}),
2801
+ ...(user !== null ? { user } : {}),
2802
+ request: req,
2803
+ });
2804
+ if (!update) {
2805
+ return { ok: false, status: 422, error: `Field "${body.changed}" not found on form "${body.formId}"` };
2806
+ }
2807
+ // Re-resolve the form with the mutated values bound. We bind
2808
+ // `$get / $set` against the post-update values map so further
2809
+ // resolve-time logic (SelectField.options(fn), reactive
2810
+ // visibility) reads current state.
2811
+ const $get = (name) => update.values[name];
2812
+ // $set on the resolve pass is a no-op — only afterStateUpdated
2813
+ // mutations survive into the response. Resolve-time `$set` would
2814
+ // race against the client's view of the world.
2815
+ const $set = (_name, _v) => { };
2816
+ const resolveCtx = {
2817
+ ...baseCtx,
2818
+ values: update.values,
2819
+ $get,
2820
+ $set,
2821
+ changed: body.changed,
2822
+ ...(record !== undefined ? { record } : {}),
2823
+ };
2824
+ // Snapshot values onto the form so its FormMeta carries them.
2825
+ form.withValues(update.values);
2826
+ const resolved = await resolveSchema([form], resolveCtx);
2827
+ const formMeta = resolved[0];
2828
+ if (!formMeta || formMeta.type !== 'form') {
2829
+ return { ok: false, status: 422, error: 'Form re-resolved to non-form meta' };
2830
+ }
2831
+ return { ok: true, form: formMeta, dirty: update.dirty };
2832
+ }
2833
+ /**
2834
+ * Plan #8 — handle a Wizard step-validate POST. Locates the form by id,
2835
+ * walks to the Wizard descendant, validates only the fields inside step
2836
+ * `step` against `values`. Returns `{ ok: true }` on success or
2837
+ * `{ ok: false, status: 422, errors }` when fields fail validation.
2838
+ *
2839
+ * Errors are keyed by field name, same shape as the form-submit 422 path,
2840
+ * so the client (`FormStateApi.applyErrors`) can surface them in-place.
2841
+ */
2842
+ export async function formWizardData(pilotiq, scope, body, req) {
2843
+ const cfg = pilotiq.getConfig();
2844
+ const user = await pilotiq.resolveUser(req);
2845
+ let PageClass;
2846
+ let mode;
2847
+ let record = undefined;
2848
+ let baseCtxExtras = {};
2849
+ if (scope.kind === 'resource-create' || scope.kind === 'resource-edit') {
2850
+ const R = cfg.resources.find(r => r.getSlug() === scope.slug);
2851
+ if (!R)
2852
+ return null;
2853
+ const pages = R.resolvePages();
2854
+ if (scope.kind === 'resource-create') {
2855
+ if (!pages.create)
2856
+ return null;
2857
+ PageClass = pages.create;
2858
+ mode = 'create';
2859
+ }
2860
+ else {
2861
+ if (!pages.edit)
2862
+ return null;
2863
+ PageClass = pages.edit;
2864
+ mode = 'edit';
2865
+ baseCtxExtras = { recordId: scope.recordId };
2866
+ if (R.model) {
2867
+ try {
2868
+ record = await findRecord(R, scope.recordId, { user });
2869
+ }
2870
+ catch { /* ignore */ }
2871
+ }
2872
+ else {
2873
+ record = { id: scope.recordId };
2874
+ }
2875
+ }
2876
+ }
2877
+ else if (scope.kind === 'global-edit') {
2878
+ const G = cfg.globals.find(g => g.getSlug() === scope.slug);
2879
+ if (!G)
2880
+ return null;
2881
+ const pages = G.resolvePages();
2882
+ if (!pages.edit)
2883
+ return null;
2884
+ PageClass = pages.edit;
2885
+ mode = 'edit';
2886
+ }
2887
+ else {
2888
+ const P = cfg.pages.find(p => p.getSlug() === scope.pageSlug);
2889
+ if (!P)
2890
+ return null;
2891
+ PageClass = P;
2892
+ mode = 'edit';
2893
+ }
2894
+ if (!PageClass)
2895
+ return null;
2896
+ const baseCtx = uploadCtx(userCtx({ mode, basePath: cfg.path, ...baseCtxExtras }, user), cfg);
2897
+ const elements = await callPageSchema(PageClass, baseCtx);
2898
+ const form = selectFormById(findForms(elements), body.formId);
2899
+ if (!form)
2900
+ return { ok: false, status: 404, error: `Form "${body.formId}" not found on page` };
2901
+ const formChildren = form.getChildren() ?? [];
2902
+ const stepFields = findWizardStepFields(formChildren, body.step);
2903
+ if (!stepFields)
2904
+ return { ok: false, status: 404, error: `Step ${body.step} not found on form "${body.formId}"` };
2905
+ const errors = await validateSchema(stepFields, body.values, record);
2906
+ if (Object.keys(errors).length > 0) {
2907
+ return { ok: false, status: 422, errors };
2908
+ }
2909
+ return { ok: true };
2910
+ }
2911
+ /** Find a `SelectField` by name inside a form's children, walking through
2912
+ * layout containers but stopping at Repeater / Builder boundaries
2913
+ * (parallel to `tagSelectCreateOptionUrls`'s walker). Returns the first
2914
+ * match or `undefined`. */
2915
+ function findSelectFieldByName(elements, name) {
2916
+ for (const el of elements) {
2917
+ if (el instanceof SelectField) {
2918
+ if (el.name === name)
2919
+ return el;
2920
+ continue;
2921
+ }
2922
+ if (el instanceof RepeaterField)
2923
+ continue;
2924
+ if (el instanceof BuilderField)
2925
+ continue;
2926
+ const children = el.getChildren();
2927
+ if (children && children.length > 0) {
2928
+ const found = findSelectFieldByName(children, name);
2929
+ if (found)
2930
+ return found;
2931
+ }
2932
+ }
2933
+ return undefined;
2934
+ }
2935
+ /**
2936
+ * Audit row 2026-05-07 cont'd⁸ — handle a `SelectField.createOptionForm()`
2937
+ * modal submit. Locates the parent form by `formId`, finds the SelectField
2938
+ * by `fieldName`, re-evaluates the `createOptionAuthorize` rule (so a
2939
+ * tampered URL can't bypass), coerces + validates the body against the
2940
+ * sub-form's fields, then calls `createOptionUsing(handler)` and returns
2941
+ * `{ option }` for the client to append + select.
2942
+ *
2943
+ * Returns `null` when the route prefix doesn't resolve to a real
2944
+ * resource/global/page (route handler turns into 404).
2945
+ */
2946
+ export async function formCreateOptionData(pilotiq, scope, body, req) {
2947
+ const cfg = pilotiq.getConfig();
2948
+ const user = await pilotiq.resolveUser(req);
2949
+ let PageClass;
2950
+ let mode;
2951
+ let record = undefined;
2952
+ let baseCtxExtras = {};
2953
+ if (scope.kind === 'resource-create' || scope.kind === 'resource-edit') {
2954
+ const R = cfg.resources.find(r => r.getSlug() === scope.slug);
2955
+ if (!R)
2956
+ return null;
2957
+ const pages = R.resolvePages();
2958
+ if (scope.kind === 'resource-create') {
2959
+ if (!pages.create)
2960
+ return null;
2961
+ PageClass = pages.create;
2962
+ mode = 'create';
2963
+ }
2964
+ else {
2965
+ if (!pages.edit)
2966
+ return null;
2967
+ PageClass = pages.edit;
2968
+ mode = 'edit';
2969
+ baseCtxExtras = { recordId: scope.recordId };
2970
+ if (R.model) {
2971
+ try {
2972
+ record = await findRecord(R, scope.recordId, { user });
2973
+ }
2974
+ catch { /* ignore */ }
2975
+ }
2976
+ else {
2977
+ record = { id: scope.recordId };
2978
+ }
2979
+ }
2980
+ }
2981
+ else if (scope.kind === 'global-edit') {
2982
+ const G = cfg.globals.find(g => g.getSlug() === scope.slug);
2983
+ if (!G)
2984
+ return null;
2985
+ const pages = G.resolvePages();
2986
+ if (!pages.edit)
2987
+ return null;
2988
+ PageClass = pages.edit;
2989
+ mode = 'edit';
2990
+ }
2991
+ else {
2992
+ const P = cfg.pages.find(p => p.getSlug() === scope.pageSlug);
2993
+ if (!P)
2994
+ return null;
2995
+ PageClass = P;
2996
+ mode = 'edit';
2997
+ }
2998
+ if (!PageClass)
2999
+ return null;
3000
+ const baseCtx = uploadCtx(userCtx({ mode, basePath: cfg.path, ...baseCtxExtras }, user), cfg);
3001
+ const elements = await callPageSchema(PageClass, baseCtx);
3002
+ const form = selectFormById(findForms(elements), body.formId);
3003
+ if (!form)
3004
+ return { ok: false, status: 404, error: `Form "${body.formId}" not found on page` };
3005
+ const field = findSelectFieldByName(form.getChildren() ?? [], body.fieldName);
3006
+ if (!field)
3007
+ return { ok: false, status: 404, error: `SelectField "${body.fieldName}" not found on form "${body.formId}"` };
3008
+ if (!field.hasCreateOption())
3009
+ return { ok: false, status: 404, error: `SelectField "${body.fieldName}" does not configure createOptionForm()` };
3010
+ const createForm = field.getCreateOptionForm();
3011
+ const handler = field.getCreateOptionHandler();
3012
+ if (!handler) {
3013
+ return { ok: false, status: 500, error: `SelectField "${body.fieldName}" has createOptionForm() but no createOptionUsing() handler` };
3014
+ }
3015
+ // Re-evaluate authorize. Build the same ActionVisibilityContext shape
3016
+ // the field's `toMeta` did — keeps server / meta-build paths consistent.
3017
+ const authorize = field.getCreateOptionAuthorize();
3018
+ if (authorize !== undefined) {
3019
+ const authVisible = await (async () => {
3020
+ if (typeof authorize !== 'function')
3021
+ return authorize;
3022
+ const visCtx = {};
3023
+ if (record !== undefined)
3024
+ visCtx.record = record;
3025
+ if (user !== null)
3026
+ visCtx.user = user;
3027
+ try {
3028
+ return await authorize(visCtx);
3029
+ }
3030
+ catch {
3031
+ return false;
3032
+ }
3033
+ })();
3034
+ if (!authVisible)
3035
+ return { ok: false, status: 403, error: 'createOptionAuthorize denied' };
3036
+ }
3037
+ // Coerce + validate body against the sub-form's fields. The createOption
3038
+ // sub-schema is detached from the parent form so we run it against its
3039
+ // own children only — coerceFormValues mutates `out` to normalize toggle
3040
+ // / number / date / etc. shapes (same shape parent forms use).
3041
+ const coerced = coerceFormValues(createForm, { ...body.values });
3042
+ const errors = await validateSchema(createForm, coerced, undefined);
3043
+ if (Object.keys(errors).length > 0) {
3044
+ return { ok: false, status: 422, errors };
3045
+ }
3046
+ const ctx = {
3047
+ ...baseCtx,
3048
+ values: coerced,
3049
+ ...(record !== undefined ? { record } : {}),
3050
+ };
3051
+ let option;
3052
+ try {
3053
+ option = await handler(coerced, ctx);
3054
+ }
3055
+ catch (e) {
3056
+ return { ok: false, status: 500, error: e instanceof Error ? e.message : String(e) };
3057
+ }
3058
+ if (!option || typeof option.value !== 'string' || typeof option.label !== 'string') {
3059
+ return { ok: false, status: 500, error: `createOptionUsing must return { value: string, label: string }` };
3060
+ }
3061
+ return { ok: true, option };
3062
+ }
3063
+ function isMentionResolverField(el) {
3064
+ if (el.getType() !== 'richtext')
3065
+ return false;
3066
+ const candidate = el;
3067
+ return typeof candidate.resolveMention === 'function';
3068
+ }
3069
+ /**
3070
+ * Walk a form's tree looking for the named field. Descends into Repeater /
3071
+ * Builder rows when the requested name carries the row-prefix shape:
3072
+ *
3073
+ * - Repeater rows: `<repeaterName>.<index>.<innerPath>` — looks up
3074
+ * `<innerPath>` against the Repeater's template schema. Field config
3075
+ * (providers, async resolver) is shared across rows, so any row index
3076
+ * resolves to the same template field.
3077
+ * - Builder rows: `<builderName>.<index>.data.<innerPath>` — looks up
3078
+ * `<innerPath>` against every block's schema; first match wins. Block
3079
+ * schemas often share leaf names — if two blocks define a RichTextField
3080
+ * with the same name and different async-mention providers, only the
3081
+ * first block in declaration order is reachable here. Authors needing
3082
+ * per-block resolution should give the leaves distinct names.
3083
+ *
3084
+ * Mirrors the boundary-stopping posture of `findFieldByName` inside
3085
+ * `dispatchForm.ts` for top-level matches — only the dotted-prefix branch
3086
+ * crosses into row schemas.
3087
+ */
3088
+ function findRichTextFieldByName(elements, name) {
3089
+ for (const el of elements) {
3090
+ if (isMentionResolverField(el) && el.name === name) {
3091
+ return el;
3092
+ }
3093
+ if (isRepeaterField(el)) {
3094
+ const inner = stripRepeaterRowPrefix(name, el.name);
3095
+ if (inner !== undefined) {
3096
+ const hit = findRichTextFieldByName(el.getInnerSchema(), inner);
3097
+ if (hit)
3098
+ return hit;
3099
+ }
3100
+ continue;
3101
+ }
3102
+ if (isBuilderField(el)) {
3103
+ const inner = stripBuilderRowPrefix(name, el.name);
3104
+ if (inner !== undefined) {
3105
+ for (const block of el.getBlocks()) {
3106
+ const hit = findRichTextFieldByName(block.getSchema(), inner);
3107
+ if (hit)
3108
+ return hit;
3109
+ }
3110
+ }
3111
+ continue;
3112
+ }
3113
+ const children = el.getChildren();
3114
+ if (children && children.length > 0) {
3115
+ const hit = findRichTextFieldByName(children, name);
3116
+ if (hit)
3117
+ return hit;
3118
+ }
3119
+ }
3120
+ return undefined;
3121
+ }
3122
+ /**
3123
+ * `items.0.body` → `body`. Returns `undefined` when the path doesn't match
3124
+ * the `<repeaterName>.<digits>.<rest>` shape so the walker keeps searching
3125
+ * other branches instead of misinterpreting an unrelated dotted name.
3126
+ */
3127
+ function stripRepeaterRowPrefix(path, repeaterName) {
3128
+ const parts = path.split('.');
3129
+ if (parts.length < 3)
3130
+ return undefined;
3131
+ if (parts[0] !== repeaterName)
3132
+ return undefined;
3133
+ if (!/^\d+$/.test(parts[1] ?? ''))
3134
+ return undefined;
3135
+ return parts.slice(2).join('.');
3136
+ }
3137
+ /**
3138
+ * `blocks.0.data.heading` → `heading`. The literal `data` segment matches
3139
+ * Builder's wire shape (`{ __id, type, data: {…} }`) and distinguishes a
3140
+ * Builder leaf from a Repeater leaf at the same depth.
3141
+ */
3142
+ function stripBuilderRowPrefix(path, builderName) {
3143
+ const parts = path.split('.');
3144
+ if (parts.length < 4)
3145
+ return undefined;
3146
+ if (parts[0] !== builderName)
3147
+ return undefined;
3148
+ if (!/^\d+$/.test(parts[1] ?? ''))
3149
+ return undefined;
3150
+ if (parts[2] !== 'data')
3151
+ return undefined;
3152
+ return parts.slice(3).join('.');
3153
+ }
3154
+ /**
3155
+ * Resolve one async-mention round-trip. Locates the page's schema, finds
3156
+ * the form by `formId` and the RichTextField by `field`, calls its
3157
+ * `resolveMention(trigger, query, ctx)`. Returns `{ ok, items }`, a 404
3158
+ * when the form / field / trigger isn't present, or `null` for a missing
3159
+ * page (the route handler turns `null` into a 404 too).
3160
+ *
3161
+ * The dispatcher is duck-typed against the contract in `@pilotiq/tiptap`'s
3162
+ * `RichTextField` — pilotiq core never imports the adapter. Any future
3163
+ * field-type that ships an async-resolve trigger can implement the same
3164
+ * shape and pick up routing for free.
3165
+ */
3166
+ export async function mentionResolveData(pilotiq, scope, body, req) {
3167
+ const cfg = pilotiq.getConfig();
3168
+ const user = await pilotiq.resolveUser(req);
3169
+ let PageClass;
3170
+ let mode;
3171
+ let record = undefined;
3172
+ let baseCtxExtras = {};
3173
+ if (scope.kind === 'resource-create' || scope.kind === 'resource-edit') {
3174
+ const R = cfg.resources.find(r => r.getSlug() === scope.slug);
3175
+ if (!R)
3176
+ return null;
3177
+ const pages = R.resolvePages();
3178
+ if (scope.kind === 'resource-create') {
3179
+ if (!pages.create)
3180
+ return null;
3181
+ PageClass = pages.create;
3182
+ mode = 'create';
3183
+ }
3184
+ else {
3185
+ if (!pages.edit)
3186
+ return null;
3187
+ PageClass = pages.edit;
3188
+ mode = 'edit';
3189
+ baseCtxExtras = { recordId: scope.recordId };
3190
+ if (R.model) {
3191
+ try {
3192
+ record = await findRecord(R, scope.recordId, { user });
3193
+ }
3194
+ catch { /* ignore */ }
3195
+ }
3196
+ else {
3197
+ record = { id: scope.recordId };
3198
+ }
3199
+ }
3200
+ }
3201
+ else if (scope.kind === 'global-edit') {
3202
+ const G = cfg.globals.find(g => g.getSlug() === scope.slug);
3203
+ if (!G)
3204
+ return null;
3205
+ const pages = G.resolvePages();
3206
+ if (!pages.edit)
3207
+ return null;
3208
+ PageClass = pages.edit;
3209
+ mode = 'edit';
3210
+ }
3211
+ else {
3212
+ const P = cfg.pages.find(p => p.getSlug() === scope.pageSlug);
3213
+ if (!P)
3214
+ return null;
3215
+ PageClass = P;
3216
+ mode = 'edit';
3217
+ }
3218
+ if (!PageClass)
3219
+ return null;
3220
+ const baseCtx = uploadCtx(userCtx({ mode, basePath: cfg.path, ...baseCtxExtras }, user), cfg);
3221
+ const elements = await callPageSchema(PageClass, baseCtx);
3222
+ const form = selectFormById(findForms(elements), body.formId);
3223
+ if (!form)
3224
+ return { ok: false, status: 404, error: `Form "${body.formId}" not found on page` };
3225
+ const field = findRichTextFieldByName(form.getChildren() ?? [], body.field);
3226
+ if (!field) {
3227
+ return { ok: false, status: 404, error: `Rich-text field "${body.field}" not found on form "${body.formId}"` };
3228
+ }
3229
+ let items;
3230
+ try {
3231
+ items = await field.resolveMention(body.trigger, body.query, {
3232
+ ...(record !== undefined ? { record } : {}),
3233
+ ...(user !== null ? { user } : {}),
3234
+ request: req,
3235
+ });
3236
+ }
3237
+ catch (err) {
3238
+ return {
3239
+ ok: false,
3240
+ status: 422,
3241
+ error: err instanceof Error ? err.message : 'Mention resolver threw',
3242
+ };
3243
+ }
3244
+ if (items === null) {
3245
+ return { ok: false, status: 404, error: `No mention provider for trigger "${body.trigger}" on field "${body.field}"` };
3246
+ }
3247
+ return { ok: true, items };
3248
+ }
3249
+ export async function resourceViewData(pilotiq, slug, recordId, req) {
3250
+ const cfg = pilotiq.getConfig();
3251
+ const R = cfg.resources.find(r => r.getSlug() === slug);
3252
+ if (!R)
3253
+ return null;
3254
+ const pages = R.resolvePages();
3255
+ if (!pages.view)
3256
+ return null;
3257
+ const PageClass = pages.view;
3258
+ const user = await pilotiq.resolveUser(req);
3259
+ const ctx = uploadCtx(userCtx({ mode: 'view', recordId, basePath: cfg.path }, user), cfg);
3260
+ const elements = await callPageSchema(PageClass, ctx);
3261
+ // For the view page we want the record threaded into resolveSchema so
3262
+ // factory-attached visibility predicates see it. Resource.detail()
3263
+ // already runs against the loaded record in user code; here we mirror
3264
+ // that into ctx.record for the action eval pass.
3265
+ let record = undefined;
3266
+ if (R.model) {
3267
+ try {
3268
+ record = await findRecord(R, recordId, { user });
3269
+ }
3270
+ catch { /* ignore */ }
3271
+ }
3272
+ // Plan #11 — prepend the relation tabs strip with the "Details" tab
3273
+ // active when the resource has relation managers configured.
3274
+ const relationTabsEl = buildRelationTabs(R, recordId, cfg.path, '__view');
3275
+ if (relationTabsEl)
3276
+ elements.unshift(relationTabsEl);
3277
+ const recordTitle = record !== undefined && record !== null
3278
+ ? deriveParentTitle(R, record)
3279
+ : recordId;
3280
+ const breadcrumbs = resourceViewBreadcrumbs(cfg, R, recordTitle);
3281
+ if (breadcrumbs)
3282
+ elements.unshift(breadcrumbs);
3283
+ const viewRoute = { resource: R, page: PageClass, recordId };
3284
+ const schemaData = await applyRoleHooks(pilotiq, user, 'view', await resolveSchema(elements, record !== undefined ? { ...ctx, record } : ctx), viewRoute);
3285
+ return {
3286
+ panel: await panelInfo(pilotiq, req, viewRoute),
3287
+ page: PageClass.toMeta(),
3288
+ resource: { name: R.name, label: R.labelSingular, slug, icon: serializeIcon(R.icon, R.name) },
3289
+ mode: 'view',
3290
+ recordId,
3291
+ basePath: cfg.path,
3292
+ layout: cfg.layout,
3293
+ schemaData,
3294
+ notifications: consumeFlashedNotifications(req),
3295
+ };
3296
+ }
3297
+ export async function globalEditData(pilotiq, slug, prefill, req) {
3298
+ const cfg = pilotiq.getConfig();
3299
+ const G = cfg.globals.find(g => g.getSlug() === slug);
3300
+ if (!G)
3301
+ return null;
3302
+ const pages = G.resolvePages();
3303
+ if (!pages.edit)
3304
+ return null;
3305
+ const PageClass = pages.edit;
3306
+ const editUrl = globalBasePath(cfg.path, G);
3307
+ const user = await pilotiq.resolveUser(req);
3308
+ const ctx = uploadCtx(userCtx({ mode: 'edit', basePath: cfg.path }, user), cfg);
3309
+ const elements = await callPageSchema(PageClass, ctx);
3310
+ tagFormActions(elements, editUrl);
3311
+ tagFormStateUrls(elements, formId => `${editUrl}/_form/${formId}/state`);
3312
+ tagFormWizardUrls(elements, formId => `${editUrl}/_form/${formId}/wizard`);
3313
+ tagRichTextMentionUrls(elements, formId => `${editUrl}/_form/${formId}/mentions`);
3314
+ tagSelectCreateOptionUrls(elements, (formId, fieldName) => `${editUrl}/_form/${formId}/create-option/${fieldName}`);
3315
+ const form = findForms(elements)[0];
3316
+ let record = undefined;
3317
+ if (form?.getLoadRecord()) {
3318
+ try {
3319
+ record = await form.getLoadRecord()('', { values: prefill?.values ?? {} });
3320
+ }
3321
+ catch { /* ignore */ }
3322
+ if (!prefill?.values && record != null) {
3323
+ const values = await applyFillPipeline(form, record);
3324
+ form.withValues(values);
3325
+ }
3326
+ else if (prefill?.values) {
3327
+ form.withValues(prefill.values);
3328
+ }
3329
+ if (prefill?.errors)
3330
+ form.withErrors(prefill.errors);
3331
+ }
3332
+ const breadcrumbs = globalBreadcrumbs(cfg, G);
3333
+ if (breadcrumbs)
3334
+ elements.unshift(breadcrumbs);
3335
+ const globalEditRoute = { global: G, page: PageClass };
3336
+ const schemaData = await applyRoleHooks(pilotiq, user, 'global-edit', await resolveSchema(elements, record !== undefined ? { ...ctx, record } : ctx), globalEditRoute);
3337
+ return {
3338
+ pageType: 'global',
3339
+ panel: await panelInfo(pilotiq, req, globalEditRoute),
3340
+ page: PageClass.toMeta(),
3341
+ global: { name: G.name, label: G.label, labelSingular: G.labelSingular, slug, icon: serializeIcon(G.icon, G.name) },
3342
+ basePath: cfg.path,
3343
+ layout: cfg.layout,
3344
+ schemaData,
3345
+ notifications: consumeFlashedNotifications(req),
3346
+ ...(prefill?.errors ? { hasErrors: true } : {}),
3347
+ };
3348
+ }
3349
+ export async function globalViewData(pilotiq, slug, req) {
3350
+ const cfg = pilotiq.getConfig();
3351
+ const G = cfg.globals.find(g => g.getSlug() === slug);
3352
+ if (!G)
3353
+ return null;
3354
+ const pages = G.resolvePages();
3355
+ if (!pages.view)
3356
+ return null;
3357
+ const PageClass = pages.view;
3358
+ const user = await pilotiq.resolveUser(req);
3359
+ const ctx = uploadCtx(userCtx({ mode: 'view', basePath: cfg.path }, user), cfg);
3360
+ const elements = await callPageSchema(PageClass, ctx);
3361
+ const breadcrumbs = globalBreadcrumbs(cfg, G);
3362
+ if (breadcrumbs)
3363
+ elements.unshift(breadcrumbs);
3364
+ const globalViewRoute = { global: G, page: PageClass };
3365
+ const schemaData = await applyRoleHooks(pilotiq, user, 'global-view', await resolveSchema(elements, ctx), globalViewRoute);
3366
+ return {
3367
+ panel: await panelInfo(pilotiq, req, globalViewRoute),
3368
+ page: PageClass.toMeta(),
3369
+ global: { name: G.name, label: G.label, labelSingular: G.labelSingular, slug, icon: serializeIcon(G.icon, G.name) },
3370
+ basePath: cfg.path,
3371
+ layout: cfg.layout,
3372
+ schemaData,
3373
+ notifications: consumeFlashedNotifications(req),
3374
+ };
3375
+ }
3376
+ export async function customPageData(pilotiq, pageSlug, req) {
3377
+ const cfg = pilotiq.getConfig();
3378
+ const PageClass = cfg.pages.find(P => P.getSlug() === pageSlug);
3379
+ if (!PageClass)
3380
+ return null;
3381
+ const pageUrl = pageBasePath(cfg.path, PageClass);
3382
+ const user = await pilotiq.resolveUser(req);
3383
+ const ctx = uploadCtx(userCtx({}, user), cfg);
3384
+ const elements = await callPageSchema(PageClass, ctx);
3385
+ tagFormActions(elements, pageUrl);
3386
+ tagFormStateUrls(elements, formId => `${pageUrl}/_form/${formId}/state`);
3387
+ tagFormWizardUrls(elements, formId => `${pageUrl}/_form/${formId}/wizard`);
3388
+ tagRichTextMentionUrls(elements, formId => `${pageUrl}/_form/${formId}/mentions`);
3389
+ tagSelectCreateOptionUrls(elements, (formId, fieldName) => `${pageUrl}/_form/${formId}/create-option/${fieldName}`);
3390
+ tagActionDispatch(elements, pageUrl);
3391
+ // Page-scope polling URL (mirrors `${base}/${pageSlug}/_widget/:id`
3392
+ // route registered in routes.ts).
3393
+ tagWidgetUrls(elements, id => `${pageUrl}/_widget/${id}`);
3394
+ const widgetData = await resolveServerDataElements(elements, ctx);
3395
+ const breadcrumbs = customPageBreadcrumbs(cfg, PageClass);
3396
+ if (breadcrumbs)
3397
+ elements.unshift(breadcrumbs);
3398
+ const customRoute = { page: PageClass };
3399
+ const schemaData = await applyRoleHooks(pilotiq, user, 'page', await resolveSchema(elements, ctx), customRoute);
3400
+ return {
3401
+ pageType: 'page',
3402
+ panel: await panelInfo(pilotiq, req, customRoute),
3403
+ page: PageClass.toMeta(),
3404
+ schemaData,
3405
+ _widgetData: widgetData,
3406
+ basePath: cfg.path,
3407
+ layout: cfg.layout,
3408
+ notifications: consumeFlashedNotifications(req),
3409
+ };
3410
+ }
3411
+ /**
3412
+ * Plan #15 — re-resolve the active page's schema, find the widget by
3413
+ * id, fail-closed via `evaluateVisibility`, then run
3414
+ * `resolveServerData(ctx)` and return the payload.
3415
+ *
3416
+ * - 404 when the page or widget id doesn't exist.
3417
+ * - 403 when the layout-level `visible(rule)` says the widget is
3418
+ * hidden (server doesn't show data for hidden surfaces).
3419
+ * - 500 when the hook itself throws.
3420
+ *
3421
+ * `body.filter` rides along on `RenderContext.filter` so per-chart
3422
+ * filter dropdowns can re-fetch with the new filter value. Treated as
3423
+ * an opaque string — widget hooks decode it however they want.
3424
+ */
3425
+ export async function widgetData(pilotiq, scope, body, req) {
3426
+ const cfg = pilotiq.getConfig();
3427
+ const user = await pilotiq.resolveUser(req);
3428
+ let elements;
3429
+ let ctx;
3430
+ if (scope.kind === 'panel') {
3431
+ if (!cfg.dashboardPage)
3432
+ return { ok: false, status: 404, error: 'No dashboard page registered' };
3433
+ ctx = uploadCtx(userCtx({ basePath: cfg.path }, user), cfg);
3434
+ elements = await callPageSchema(cfg.dashboardPage, ctx);
3435
+ }
3436
+ else if (scope.kind === 'page') {
3437
+ const P = cfg.pages.find(p => p.getSlug() === scope.pageSlug);
3438
+ if (!P)
3439
+ return { ok: false, status: 404, error: 'Page not found' };
3440
+ ctx = uploadCtx(userCtx({ basePath: cfg.path }, user), cfg);
3441
+ elements = await callPageSchema(P, ctx);
3442
+ }
3443
+ else {
3444
+ // Resource-scope: re-resolve the list page's schema so widgets from
3445
+ // `Resource.headerSchema()` / `footerSchema()` are reachable.
3446
+ const R = cfg.resources.find(r => r.getSlug() === scope.slug);
3447
+ if (!R)
3448
+ return { ok: false, status: 404, error: 'Resource not found' };
3449
+ const pages = R.resolvePages();
3450
+ if (!pages.index)
3451
+ return { ok: false, status: 404, error: 'Resource has no list page' };
3452
+ ctx = uploadCtx(userCtx({ mode: 'table', basePath: cfg.path }, user), cfg);
3453
+ elements = await callPageSchema(pages.index, ctx);
3454
+ }
3455
+ // Stamp the request's filter onto the render context so widget hooks
3456
+ // can branch on it. Opaque string — widgets decode their own format.
3457
+ if (body.filter !== undefined)
3458
+ ctx = { ...ctx, filter: body.filter };
3459
+ const widget = findWidgetById(elements, body.id);
3460
+ if (!widget)
3461
+ return { ok: false, status: 404, error: `Widget "${body.id}" not found` };
3462
+ // Layout-level visibility re-check — if the widget is hidden by a
3463
+ // visible(rule), refuse to ship data. Same fail-closed posture as
3464
+ // the schema resolver. (Parent-container `visible(false)` would
3465
+ // already drop the widget from the schema tree at SSR time, so a
3466
+ // direct hidden-widget probe here covers the visible-rule-only case.)
3467
+ const layoutCtx = {};
3468
+ if (user !== null && user !== undefined)
3469
+ layoutCtx.user = user;
3470
+ if (!await widget.evaluateVisibility(layoutCtx)) {
3471
+ return { ok: false, status: 403, error: 'Widget hidden' };
3472
+ }
3473
+ try {
3474
+ const data = await widget.resolveServerData(ctx);
3475
+ return { ok: true, data, timestamp: Date.now() };
3476
+ }
3477
+ catch (err) {
3478
+ return {
3479
+ ok: false,
3480
+ status: 500,
3481
+ error: err instanceof Error ? err.message : 'Widget failed',
3482
+ };
3483
+ }
3484
+ }
3485
+ /** Walk the element tree looking for a server-data element with the
3486
+ * given id. Same walker as `collectServerDataElements` but stops on
3487
+ * first match. */
3488
+ function findWidgetById(elements, id) {
3489
+ let found;
3490
+ const walk = (els) => {
3491
+ for (const el of els) {
3492
+ if (found)
3493
+ return;
3494
+ if (isServerDataElement(el)) {
3495
+ if (el.getId() === id) {
3496
+ found = el;
3497
+ return;
3498
+ }
3499
+ continue;
3500
+ }
3501
+ const type = el.getType();
3502
+ if (type === 'form' || type === 'repeater' || type === 'builder' || type === 'table' || type === 'tableWidget')
3503
+ continue;
3504
+ const children = el.getChildren();
3505
+ if (children)
3506
+ walk(children);
3507
+ }
3508
+ };
3509
+ walk(elements);
3510
+ return found;
3511
+ }
3512
+ // ─── Plan #12 global search data builder ─────────────────────
3513
+ /**
3514
+ * Resolve the user via `pilotiq.resolveUser(req)` and run the
3515
+ * panel-wide search. Mirrors the formStateData/formWizardData
3516
+ * shape so the `/_search` route handler stays a thin wrapper.
3517
+ *
3518
+ * Also resolves the `panels::global-search.results.before/.after`
3519
+ * render hooks when the panel registered any — sparse, absent when
3520
+ * neither slot has registered fns. Sent as a `RenderHookMap` so the
3521
+ * client `<CommandPalette>` can mount `<RenderHookSlot>` above and
3522
+ * below the result list (same pattern chrome slots use).
3523
+ */
3524
+ export async function searchData(pilotiq, query, req) {
3525
+ const user = await pilotiq.resolveUser(req);
3526
+ const results = await searchAllResources(pilotiq, query, user);
3527
+ const cfg = pilotiq.getConfig();
3528
+ const out = {
3529
+ ok: true,
3530
+ results,
3531
+ };
3532
+ if (cfg.renderHooks && cfg.renderHooks.length > 0) {
3533
+ const hooks = await resolvePageHooks(pilotiq, user, pageHooksFor('search'), { url: `${cfg.path}/_search` });
3534
+ if (Object.keys(hooks).length > 0)
3535
+ out.renderHooks = hooks;
3536
+ }
3537
+ return out;
3538
+ }
3539
+ /**
3540
+ * Single entry point Vike's `+data` hook calls. Inspects the page id and
3541
+ * route params, finds the panel via `PilotiqRegistry`, and dispatches to
3542
+ * the matching builder. Returns the same shape SSR's `viewProps` carries.
3543
+ */
3544
+ export async function dispatchPageData(pageContext) {
3545
+ const { pageId, routeParams = {} } = pageContext;
3546
+ const search = pageContext.urlParsed?.search ?? {};
3547
+ const basePathParam = routeParams['basePath'];
3548
+ const basePath = basePathParam ? `/${basePathParam}` : '';
3549
+ const panel = basePath ? PilotiqRegistry.findByPath(basePath) : null;
3550
+ if (!panel)
3551
+ return null;
3552
+ switch (pageId) {
3553
+ case '/pages/(pilotiq)/dashboard':
3554
+ return dashboardData(panel);
3555
+ case '/pages/(pilotiq)/slug': {
3556
+ // 2-segment URL: could be a resource list, a global edit, or a custom page.
3557
+ const slug = routeParams['slug'];
3558
+ if (!slug)
3559
+ return null;
3560
+ const cfg = panel.getConfig();
3561
+ if (cfg.resources.some(R => R.getSlug() === slug)) {
3562
+ return resourceIndexData(panel, slug, search);
3563
+ }
3564
+ if (cfg.globals.some(G => G.getSlug() === slug)) {
3565
+ return globalEditData(panel, slug);
3566
+ }
3567
+ return customPageData(panel, slug);
3568
+ }
3569
+ case '/pages/(pilotiq)/resource-create': {
3570
+ const slug = routeParams['slug'];
3571
+ if (!slug)
3572
+ return null;
3573
+ return resourceCreateData(panel, slug);
3574
+ }
3575
+ case '/pages/(pilotiq)/resource-edit': {
3576
+ const slug = routeParams['slug'];
3577
+ const id = routeParams['id'];
3578
+ if (!slug || !id)
3579
+ return null;
3580
+ return resourceEditData(panel, slug, id);
3581
+ }
3582
+ case '/pages/(pilotiq)/resource-view': {
3583
+ const slug = routeParams['slug'];
3584
+ const id = routeParams['id'];
3585
+ if (!slug)
3586
+ return null;
3587
+ // Globals also use this route under `/{slug}/view` — id will be 'view'.
3588
+ if (id === 'view')
3589
+ return globalViewData(panel, slug);
3590
+ if (!id)
3591
+ return null;
3592
+ return resourceViewData(panel, slug, id);
3593
+ }
3594
+ case '/pages/(pilotiq)/relation-list': {
3595
+ const slug = routeParams['slug'];
3596
+ const id = routeParams['id'];
3597
+ const relationship = routeParams['relationship'];
3598
+ if (!slug || !id || !relationship)
3599
+ return null;
3600
+ const out = await relationManagerData(panel, {
3601
+ kind: 'relation-list', slug, recordId: id, relationship,
3602
+ query: search,
3603
+ });
3604
+ // Tagged failure shapes (`{ ok: false, status: 403 }`) leak straight
3605
+ // through to the +Page renderer, which can branch on the shape.
3606
+ // For Plan #11 we let null short-circuit the SPA render the same
3607
+ // way the resource builders do.
3608
+ return out === null ? null : out;
3609
+ }
3610
+ case '/pages/(pilotiq)/relation-create': {
3611
+ const slug = routeParams['slug'];
3612
+ const id = routeParams['id'];
3613
+ const relationship = routeParams['relationship'];
3614
+ if (!slug || !id || !relationship)
3615
+ return null;
3616
+ const out = await relationManagerData(panel, {
3617
+ kind: 'relation-create', slug, recordId: id, relationship,
3618
+ });
3619
+ return out === null ? null : out;
3620
+ }
3621
+ case '/pages/(pilotiq)/relation-view': {
3622
+ const slug = routeParams['slug'];
3623
+ const id = routeParams['id'];
3624
+ const relationship = routeParams['relationship'];
3625
+ const childId = routeParams['childId'];
3626
+ if (!slug || !id || !relationship || !childId)
3627
+ return null;
3628
+ const out = await relationManagerData(panel, {
3629
+ kind: 'relation-view', slug, recordId: id, relationship, childId,
3630
+ });
3631
+ return out === null ? null : out;
3632
+ }
3633
+ case '/pages/(pilotiq)/relation-edit': {
3634
+ const slug = routeParams['slug'];
3635
+ const id = routeParams['id'];
3636
+ const relationship = routeParams['relationship'];
3637
+ const childId = routeParams['childId'];
3638
+ if (!slug || !id || !relationship || !childId)
3639
+ return null;
3640
+ const out = await relationManagerData(panel, {
3641
+ kind: 'relation-edit', slug, recordId: id, relationship, childId,
3642
+ });
3643
+ return out === null ? null : out;
3644
+ }
3645
+ // Phase B nested-relation routes. Param names match those declared
3646
+ // by the auto-gen Vike stubs in `src/vite.ts`:
3647
+ // id, relationship, childId1, relationship2, childId2.
3648
+ case '/pages/(pilotiq)/nested-relation-list': {
3649
+ const slug = routeParams['slug'];
3650
+ const id = routeParams['id'];
3651
+ const relationship = routeParams['relationship'];
3652
+ const childId1 = routeParams['childId1'];
3653
+ const relationship2 = routeParams['relationship2'];
3654
+ if (!slug || !id || !relationship || !childId1 || !relationship2)
3655
+ return null;
3656
+ const out = await relationManagerData(panel, {
3657
+ kind: 'nested-relation-list', slug,
3658
+ chain: [
3659
+ { recordId: id, relationship },
3660
+ { recordId: childId1, relationship: relationship2 },
3661
+ ],
3662
+ query: search,
3663
+ });
3664
+ return out === null ? null : out;
3665
+ }
3666
+ case '/pages/(pilotiq)/nested-relation-create': {
3667
+ const slug = routeParams['slug'];
3668
+ const id = routeParams['id'];
3669
+ const relationship = routeParams['relationship'];
3670
+ const childId1 = routeParams['childId1'];
3671
+ const relationship2 = routeParams['relationship2'];
3672
+ if (!slug || !id || !relationship || !childId1 || !relationship2)
3673
+ return null;
3674
+ const out = await relationManagerData(panel, {
3675
+ kind: 'nested-relation-create', slug,
3676
+ chain: [
3677
+ { recordId: id, relationship },
3678
+ { recordId: childId1, relationship: relationship2 },
3679
+ ],
3680
+ });
3681
+ return out === null ? null : out;
3682
+ }
3683
+ case '/pages/(pilotiq)/nested-relation-view': {
3684
+ const slug = routeParams['slug'];
3685
+ const id = routeParams['id'];
3686
+ const relationship = routeParams['relationship'];
3687
+ const childId1 = routeParams['childId1'];
3688
+ const relationship2 = routeParams['relationship2'];
3689
+ const childId2 = routeParams['childId2'];
3690
+ if (!slug || !id || !relationship || !childId1 || !relationship2 || !childId2)
3691
+ return null;
3692
+ const out = await relationManagerData(panel, {
3693
+ kind: 'nested-relation-view', slug,
3694
+ chain: [
3695
+ { recordId: id, relationship },
3696
+ { recordId: childId1, relationship: relationship2 },
3697
+ ],
3698
+ childId: childId2,
3699
+ });
3700
+ return out === null ? null : out;
3701
+ }
3702
+ case '/pages/(pilotiq)/nested-relation-edit': {
3703
+ const slug = routeParams['slug'];
3704
+ const id = routeParams['id'];
3705
+ const relationship = routeParams['relationship'];
3706
+ const childId1 = routeParams['childId1'];
3707
+ const relationship2 = routeParams['relationship2'];
3708
+ const childId2 = routeParams['childId2'];
3709
+ if (!slug || !id || !relationship || !childId1 || !relationship2 || !childId2)
3710
+ return null;
3711
+ const out = await relationManagerData(panel, {
3712
+ kind: 'nested-relation-edit', slug,
3713
+ chain: [
3714
+ { recordId: id, relationship },
3715
+ { recordId: childId1, relationship: relationship2 },
3716
+ ],
3717
+ childId: childId2,
3718
+ });
3719
+ return out === null ? null : out;
3720
+ }
3721
+ default:
3722
+ return null;
3723
+ }
3724
+ }
3725
+ //# sourceMappingURL=pageData.js.map