@pilotiq/pilotiq 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +208 -0
  3. package/CLAUDE.md +59 -3
  4. package/dist/Pilotiq.d.ts +83 -0
  5. package/dist/Pilotiq.d.ts.map +1 -1
  6. package/dist/Pilotiq.js +39 -0
  7. package/dist/Pilotiq.js.map +1 -1
  8. package/dist/actions/Action.d.ts +27 -99
  9. package/dist/actions/Action.d.ts.map +1 -1
  10. package/dist/actions/Action.js +52 -754
  11. package/dist/actions/Action.js.map +1 -1
  12. package/dist/actions/bulkFactories.d.ts +46 -0
  13. package/dist/actions/bulkFactories.d.ts.map +1 -0
  14. package/dist/actions/bulkFactories.js +144 -0
  15. package/dist/actions/bulkFactories.js.map +1 -0
  16. package/dist/actions/crudFactories.d.ts +94 -0
  17. package/dist/actions/crudFactories.d.ts.map +1 -0
  18. package/dist/actions/crudFactories.js +209 -0
  19. package/dist/actions/crudFactories.js.map +1 -0
  20. package/dist/actions/factoryHelpers.d.ts +108 -0
  21. package/dist/actions/factoryHelpers.d.ts.map +1 -0
  22. package/dist/actions/factoryHelpers.js +138 -0
  23. package/dist/actions/factoryHelpers.js.map +1 -0
  24. package/dist/actions/m2mFactories.d.ts +47 -0
  25. package/dist/actions/m2mFactories.d.ts.map +1 -0
  26. package/dist/actions/m2mFactories.js +173 -0
  27. package/dist/actions/m2mFactories.js.map +1 -0
  28. package/dist/actions/relationFactories.d.ts +93 -0
  29. package/dist/actions/relationFactories.d.ts.map +1 -0
  30. package/dist/actions/relationFactories.js +321 -0
  31. package/dist/actions/relationFactories.js.map +1 -0
  32. package/dist/elements/dispatchForm.js +1 -1
  33. package/dist/elements/dispatchForm.js.map +1 -1
  34. package/dist/elements/dispatchTable.js +1 -1
  35. package/dist/elements/dispatchTable.js.map +1 -1
  36. package/dist/fields/Field.d.ts +31 -0
  37. package/dist/fields/Field.d.ts.map +1 -1
  38. package/dist/fields/Field.js +25 -0
  39. package/dist/fields/Field.js.map +1 -1
  40. package/dist/pageData/breadcrumbs.d.ts +42 -0
  41. package/dist/pageData/breadcrumbs.d.ts.map +1 -0
  42. package/dist/pageData/breadcrumbs.js +172 -0
  43. package/dist/pageData/breadcrumbs.js.map +1 -0
  44. package/dist/pageData/forms.d.ts +137 -0
  45. package/dist/pageData/forms.d.ts.map +1 -0
  46. package/dist/pageData/forms.js +427 -0
  47. package/dist/pageData/forms.js.map +1 -0
  48. package/dist/pageData/helpers.d.ts +239 -0
  49. package/dist/pageData/helpers.d.ts.map +1 -0
  50. package/dist/pageData/helpers.js +703 -0
  51. package/dist/pageData/helpers.js.map +1 -0
  52. package/dist/pageData/misc.d.ts +76 -0
  53. package/dist/pageData/misc.d.ts.map +1 -0
  54. package/dist/pageData/misc.js +263 -0
  55. package/dist/pageData/misc.js.map +1 -0
  56. package/dist/pageData/navigation.d.ts +292 -0
  57. package/dist/pageData/navigation.d.ts.map +1 -0
  58. package/dist/pageData/navigation.js +591 -0
  59. package/dist/pageData/navigation.js.map +1 -0
  60. package/dist/pageData/relationPages.d.ts +172 -0
  61. package/dist/pageData/relationPages.d.ts.map +1 -0
  62. package/dist/pageData/relationPages.js +867 -0
  63. package/dist/pageData/relationPages.js.map +1 -0
  64. package/dist/pageData/relationTabs.d.ts +65 -0
  65. package/dist/pageData/relationTabs.d.ts.map +1 -0
  66. package/dist/pageData/relationTabs.js +258 -0
  67. package/dist/pageData/relationTabs.js.map +1 -0
  68. package/dist/pageData/resourcePages.d.ts +48 -0
  69. package/dist/pageData/resourcePages.d.ts.map +1 -0
  70. package/dist/pageData/resourcePages.js +504 -0
  71. package/dist/pageData/resourcePages.js.map +1 -0
  72. package/dist/pageData.d.ts +12 -792
  73. package/dist/pageData.d.ts.map +1 -1
  74. package/dist/pageData.js +24 -3797
  75. package/dist/pageData.js.map +1 -1
  76. package/dist/react/AppShell.d.ts +8 -0
  77. package/dist/react/AppShell.d.ts.map +1 -1
  78. package/dist/react/AppShell.js +11 -1
  79. package/dist/react/AppShell.js.map +1 -1
  80. package/dist/react/CollabExtensionFactoryRegistry.d.ts +47 -0
  81. package/dist/react/CollabExtensionFactoryRegistry.d.ts.map +1 -0
  82. package/dist/react/CollabExtensionFactoryRegistry.js +14 -0
  83. package/dist/react/CollabExtensionFactoryRegistry.js.map +1 -0
  84. package/dist/react/CollabRoomContext.d.ts +37 -0
  85. package/dist/react/CollabRoomContext.d.ts.map +1 -0
  86. package/dist/react/CollabRoomContext.js +12 -0
  87. package/dist/react/CollabRoomContext.js.map +1 -0
  88. package/dist/react/FormCollabBindingRegistry.d.ts +62 -0
  89. package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -0
  90. package/dist/react/FormCollabBindingRegistry.js +14 -0
  91. package/dist/react/FormCollabBindingRegistry.js.map +1 -0
  92. package/dist/react/FormStateContext.d.ts.map +1 -1
  93. package/dist/react/FormStateContext.js +87 -0
  94. package/dist/react/FormStateContext.js.map +1 -1
  95. package/dist/react/RecordWrapperGate.d.ts +25 -0
  96. package/dist/react/RecordWrapperGate.d.ts.map +1 -0
  97. package/dist/react/RecordWrapperGate.js +30 -0
  98. package/dist/react/RecordWrapperGate.js.map +1 -0
  99. package/dist/react/RecordWrapperRegistry.d.ts +31 -0
  100. package/dist/react/RecordWrapperRegistry.d.ts.map +1 -0
  101. package/dist/react/RecordWrapperRegistry.js +15 -0
  102. package/dist/react/RecordWrapperRegistry.js.map +1 -0
  103. package/dist/react/SchemaRenderer.d.ts +17 -23
  104. package/dist/react/SchemaRenderer.d.ts.map +1 -1
  105. package/dist/react/SchemaRenderer.js +71 -3647
  106. package/dist/react/SchemaRenderer.js.map +1 -1
  107. package/dist/react/component-slots.d.ts +103 -0
  108. package/dist/react/component-slots.d.ts.map +1 -0
  109. package/dist/react/component-slots.js +18 -0
  110. package/dist/react/component-slots.js.map +1 -0
  111. package/dist/react/fields/BuilderInput.d.ts.map +1 -1
  112. package/dist/react/fields/BuilderInput.js +21 -117
  113. package/dist/react/fields/BuilderInput.js.map +1 -1
  114. package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
  115. package/dist/react/fields/MarkdownInput.js +1 -3
  116. package/dist/react/fields/MarkdownInput.js.map +1 -1
  117. package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
  118. package/dist/react/fields/RepeaterInput.js +22 -127
  119. package/dist/react/fields/RepeaterInput.js.map +1 -1
  120. package/dist/react/fields/rowState.d.ts +40 -0
  121. package/dist/react/fields/rowState.d.ts.map +1 -0
  122. package/dist/react/fields/rowState.js +60 -0
  123. package/dist/react/fields/rowState.js.map +1 -0
  124. package/dist/react/fields/useRowReorderDnd.d.ts +28 -0
  125. package/dist/react/fields/useRowReorderDnd.d.ts.map +1 -0
  126. package/dist/react/fields/useRowReorderDnd.js +51 -0
  127. package/dist/react/fields/useRowReorderDnd.js.map +1 -0
  128. package/dist/react/index.d.ts +9 -0
  129. package/dist/react/index.d.ts.map +1 -1
  130. package/dist/react/index.js +8 -0
  131. package/dist/react/index.js.map +1 -1
  132. package/dist/react/layouts/SidebarLayout.d.ts +1 -1
  133. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  134. package/dist/react/layouts/SidebarLayout.js +10 -2
  135. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  136. package/dist/react/layouts/TopbarLayout.d.ts +1 -1
  137. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -1
  138. package/dist/react/layouts/TopbarLayout.js +19 -11
  139. package/dist/react/layouts/TopbarLayout.js.map +1 -1
  140. package/dist/react/parseRecordEditUrl.d.ts +29 -0
  141. package/dist/react/parseRecordEditUrl.d.ts.map +1 -0
  142. package/dist/react/parseRecordEditUrl.js +25 -0
  143. package/dist/react/parseRecordEditUrl.js.map +1 -0
  144. package/dist/react/persistedState.d.ts +19 -0
  145. package/dist/react/persistedState.d.ts.map +1 -0
  146. package/dist/react/persistedState.js +51 -0
  147. package/dist/react/persistedState.js.map +1 -0
  148. package/dist/react/schemaRenderer/AlertRenderer.d.ts +12 -0
  149. package/dist/react/schemaRenderer/AlertRenderer.d.ts.map +1 -0
  150. package/dist/react/schemaRenderer/AlertRenderer.js +61 -0
  151. package/dist/react/schemaRenderer/AlertRenderer.js.map +1 -0
  152. package/dist/react/schemaRenderer/EntryRenderer.d.ts +13 -0
  153. package/dist/react/schemaRenderer/EntryRenderer.d.ts.map +1 -0
  154. package/dist/react/schemaRenderer/EntryRenderer.js +277 -0
  155. package/dist/react/schemaRenderer/EntryRenderer.js.map +1 -0
  156. package/dist/react/schemaRenderer/SectionRenderer.d.ts +16 -0
  157. package/dist/react/schemaRenderer/SectionRenderer.d.ts.map +1 -0
  158. package/dist/react/schemaRenderer/SectionRenderer.js +62 -0
  159. package/dist/react/schemaRenderer/SectionRenderer.js.map +1 -0
  160. package/dist/react/schemaRenderer/SimpleElements.d.ts +25 -0
  161. package/dist/react/schemaRenderer/SimpleElements.d.ts.map +1 -0
  162. package/dist/react/schemaRenderer/SimpleElements.js +147 -0
  163. package/dist/react/schemaRenderer/SimpleElements.js.map +1 -0
  164. package/dist/react/schemaRenderer/TabsRenderer.d.ts +17 -0
  165. package/dist/react/schemaRenderer/TabsRenderer.d.ts.map +1 -0
  166. package/dist/react/schemaRenderer/TabsRenderer.js +31 -0
  167. package/dist/react/schemaRenderer/TabsRenderer.js.map +1 -0
  168. package/dist/react/schemaRenderer/WizardRenderer.d.ts +34 -0
  169. package/dist/react/schemaRenderer/WizardRenderer.d.ts.map +1 -0
  170. package/dist/react/schemaRenderer/WizardRenderer.js +208 -0
  171. package/dist/react/schemaRenderer/WizardRenderer.js.map +1 -0
  172. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts +21 -0
  173. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts.map +1 -0
  174. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js +82 -0
  175. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js.map +1 -0
  176. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts +30 -0
  177. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts.map +1 -0
  178. package/dist/react/schemaRenderer/action/ActionModalDialog.js +182 -0
  179. package/dist/react/schemaRenderer/action/ActionModalDialog.js.map +1 -0
  180. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts +17 -0
  181. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts.map +1 -0
  182. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js +19 -0
  183. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js.map +1 -0
  184. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts +16 -0
  185. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts.map +1 -0
  186. package/dist/react/schemaRenderer/action/HandlerActionButton.js +16 -0
  187. package/dist/react/schemaRenderer/action/HandlerActionButton.js.map +1 -0
  188. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts +22 -0
  189. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts.map +1 -0
  190. package/dist/react/schemaRenderer/action/MethodActionButton.js +26 -0
  191. package/dist/react/schemaRenderer/action/MethodActionButton.js.map +1 -0
  192. package/dist/react/schemaRenderer/action/buttons.d.ts +18 -0
  193. package/dist/react/schemaRenderer/action/buttons.d.ts.map +1 -0
  194. package/dist/react/schemaRenderer/action/buttons.js +74 -0
  195. package/dist/react/schemaRenderer/action/buttons.js.map +1 -0
  196. package/dist/react/schemaRenderer/action/helpers.d.ts +26 -0
  197. package/dist/react/schemaRenderer/action/helpers.d.ts.map +1 -0
  198. package/dist/react/schemaRenderer/action/helpers.js +126 -0
  199. package/dist/react/schemaRenderer/action/helpers.js.map +1 -0
  200. package/dist/react/schemaRenderer/action/renderAction.d.ts +21 -0
  201. package/dist/react/schemaRenderer/action/renderAction.d.ts.map +1 -0
  202. package/dist/react/schemaRenderer/action/renderAction.js +102 -0
  203. package/dist/react/schemaRenderer/action/renderAction.js.map +1 -0
  204. package/dist/react/schemaRenderer/columnFormat.d.ts +10 -0
  205. package/dist/react/schemaRenderer/columnFormat.d.ts.map +1 -0
  206. package/dist/react/schemaRenderer/columnFormat.js +76 -0
  207. package/dist/react/schemaRenderer/columnFormat.js.map +1 -0
  208. package/dist/react/schemaRenderer/constants.d.ts +8 -0
  209. package/dist/react/schemaRenderer/constants.d.ts.map +1 -0
  210. package/dist/react/schemaRenderer/constants.js +45 -0
  211. package/dist/react/schemaRenderer/constants.js.map +1 -0
  212. package/dist/react/schemaRenderer/form/FormRenderer.d.ts +29 -0
  213. package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -0
  214. package/dist/react/schemaRenderer/form/FormRenderer.js +163 -0
  215. package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -0
  216. package/dist/react/schemaRenderer/form/renderField.d.ts +6 -0
  217. package/dist/react/schemaRenderer/form/renderField.d.ts.map +1 -0
  218. package/dist/react/schemaRenderer/form/renderField.js +239 -0
  219. package/dist/react/schemaRenderer/form/renderField.js.map +1 -0
  220. package/dist/react/schemaRenderer/helpers.d.ts +32 -0
  221. package/dist/react/schemaRenderer/helpers.d.ts.map +1 -0
  222. package/dist/react/schemaRenderer/helpers.js +52 -0
  223. package/dist/react/schemaRenderer/helpers.js.map +1 -0
  224. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts +60 -0
  225. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts.map +1 -0
  226. package/dist/react/schemaRenderer/table/CardsLayoutBody.js +189 -0
  227. package/dist/react/schemaRenderer/table/CardsLayoutBody.js.map +1 -0
  228. package/dist/react/schemaRenderer/table/TableRenderer.d.ts +29 -0
  229. package/dist/react/schemaRenderer/table/TableRenderer.d.ts.map +1 -0
  230. package/dist/react/schemaRenderer/table/TableRenderer.js +85 -0
  231. package/dist/react/schemaRenderer/table/TableRenderer.js.map +1 -0
  232. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts +18 -0
  233. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts.map +1 -0
  234. package/dist/react/schemaRenderer/table/TableRendererBody.js +555 -0
  235. package/dist/react/schemaRenderer/table/TableRendererBody.js.map +1 -0
  236. package/dist/react/schemaRenderer/table/filters.d.ts +263 -0
  237. package/dist/react/schemaRenderer/table/filters.d.ts.map +1 -0
  238. package/dist/react/schemaRenderer/table/filters.js +497 -0
  239. package/dist/react/schemaRenderer/table/filters.js.map +1 -0
  240. package/dist/react/schemaRenderer/table/formatCell.d.ts +11 -0
  241. package/dist/react/schemaRenderer/table/formatCell.d.ts.map +1 -0
  242. package/dist/react/schemaRenderer/table/formatCell.js +172 -0
  243. package/dist/react/schemaRenderer/table/formatCell.js.map +1 -0
  244. package/dist/react/schemaRenderer/table/links.d.ts +42 -0
  245. package/dist/react/schemaRenderer/table/links.d.ts.map +1 -0
  246. package/dist/react/schemaRenderer/table/links.js +55 -0
  247. package/dist/react/schemaRenderer/table/links.js.map +1 -0
  248. package/dist/react/schemaRenderer/table/renderRowActions.d.ts +13 -0
  249. package/dist/react/schemaRenderer/table/renderRowActions.d.ts.map +1 -0
  250. package/dist/react/schemaRenderer/table/renderRowActions.js +25 -0
  251. package/dist/react/schemaRenderer/table/renderRowActions.js.map +1 -0
  252. package/dist/react/schemaRenderer/table/url.d.ts +41 -0
  253. package/dist/react/schemaRenderer/table/url.d.ts.map +1 -0
  254. package/dist/react/schemaRenderer/table/url.js +114 -0
  255. package/dist/react/schemaRenderer/table/url.js.map +1 -0
  256. package/dist/routes/globals.d.ts +13 -0
  257. package/dist/routes/globals.d.ts.map +1 -0
  258. package/dist/routes/globals.js +131 -0
  259. package/dist/routes/globals.js.map +1 -0
  260. package/dist/routes/helpers.d.ts +217 -0
  261. package/dist/routes/helpers.d.ts.map +1 -0
  262. package/dist/routes/helpers.js +498 -0
  263. package/dist/routes/helpers.js.map +1 -0
  264. package/dist/routes/pages.d.ts +15 -0
  265. package/dist/routes/pages.d.ts.map +1 -0
  266. package/dist/routes/pages.js +145 -0
  267. package/dist/routes/pages.js.map +1 -0
  268. package/dist/routes/panel.d.ts +19 -0
  269. package/dist/routes/panel.d.ts.map +1 -0
  270. package/dist/routes/panel.js +191 -0
  271. package/dist/routes/panel.js.map +1 -0
  272. package/dist/routes/relations.d.ts +21 -0
  273. package/dist/routes/relations.d.ts.map +1 -0
  274. package/dist/routes/relations.js +1239 -0
  275. package/dist/routes/relations.js.map +1 -0
  276. package/dist/routes/resources.d.ts +28 -0
  277. package/dist/routes/resources.d.ts.map +1 -0
  278. package/dist/routes/resources.js +741 -0
  279. package/dist/routes/resources.js.map +1 -0
  280. package/dist/routes/theme.d.ts +12 -0
  281. package/dist/routes/theme.d.ts.map +1 -0
  282. package/dist/routes/theme.js +82 -0
  283. package/dist/routes/theme.js.map +1 -0
  284. package/dist/routes.d.ts.map +1 -1
  285. package/dist/routes.js +64 -3078
  286. package/dist/routes.js.map +1 -1
  287. package/dist/vite.d.ts +1 -0
  288. package/dist/vite.d.ts.map +1 -1
  289. package/dist/vite.js +26 -5
  290. package/dist/vite.js.map +1 -1
  291. package/package.json +2 -1
  292. package/src/Pilotiq.ts +95 -0
  293. package/src/actions/Action.ts +79 -723
  294. package/src/actions/bulkFactories.ts +168 -0
  295. package/src/actions/crudFactories.ts +220 -0
  296. package/src/actions/factoryHelpers.ts +177 -0
  297. package/src/actions/m2mFactories.ts +193 -0
  298. package/src/actions/relationFactories.ts +372 -0
  299. package/src/elements/dispatchForm.ts +1 -1
  300. package/src/elements/dispatchTable.ts +1 -1
  301. package/src/fields/Field.ts +39 -0
  302. package/src/pageData/breadcrumbs.ts +288 -0
  303. package/src/pageData/forms.ts +578 -0
  304. package/src/pageData/helpers.ts +764 -0
  305. package/src/pageData/misc.ts +347 -0
  306. package/src/pageData/navigation.ts +779 -0
  307. package/src/pageData/relationPages.ts +1246 -0
  308. package/src/pageData/relationTabs.ts +286 -0
  309. package/src/pageData/resourcePages.ts +593 -0
  310. package/src/pageData.ts +122 -4731
  311. package/src/react/AppShell.tsx +27 -1
  312. package/src/react/CollabExtensionFactoryRegistry.ts +55 -0
  313. package/src/react/CollabRoomContext.ts +42 -0
  314. package/src/react/FormCollabBindingRegistry.ts +72 -0
  315. package/src/react/FormStateContext.tsx +91 -0
  316. package/src/react/RecordWrapperGate.tsx +40 -0
  317. package/src/react/RecordWrapperRegistry.ts +39 -0
  318. package/src/react/SchemaRenderer.tsx +230 -6479
  319. package/src/react/component-slots.test.ts +103 -0
  320. package/src/react/component-slots.ts +116 -0
  321. package/src/react/fields/BuilderInput.tsx +29 -117
  322. package/src/react/fields/MarkdownInput.tsx +0 -1
  323. package/src/react/fields/RepeaterInput.tsx +29 -130
  324. package/src/react/fields/rowState.ts +106 -0
  325. package/src/react/fields/useRowReorderDnd.ts +78 -0
  326. package/src/react/index.ts +38 -0
  327. package/src/react/layouts/SidebarLayout.tsx +39 -28
  328. package/src/react/layouts/TopbarLayout.tsx +70 -57
  329. package/src/react/parseRecordEditUrl.test.ts +75 -0
  330. package/src/react/parseRecordEditUrl.ts +55 -0
  331. package/src/react/persistedState.ts +40 -0
  332. package/src/react/schemaRenderer/AlertRenderer.tsx +112 -0
  333. package/src/react/schemaRenderer/EntryRenderer.tsx +501 -0
  334. package/src/react/schemaRenderer/SectionRenderer.tsx +120 -0
  335. package/src/react/schemaRenderer/SimpleElements.tsx +306 -0
  336. package/src/react/schemaRenderer/TabsRenderer.tsx +62 -0
  337. package/src/react/schemaRenderer/WizardRenderer.tsx +338 -0
  338. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +177 -0
  339. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +273 -0
  340. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +61 -0
  341. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +43 -0
  342. package/src/react/schemaRenderer/action/MethodActionButton.tsx +64 -0
  343. package/src/react/schemaRenderer/action/buttons.tsx +99 -0
  344. package/src/react/schemaRenderer/action/helpers.ts +140 -0
  345. package/src/react/schemaRenderer/action/renderAction.tsx +245 -0
  346. package/src/react/schemaRenderer/columnFormat.ts +65 -0
  347. package/src/react/schemaRenderer/constants.ts +50 -0
  348. package/src/react/schemaRenderer/form/FormRenderer.tsx +245 -0
  349. package/src/react/schemaRenderer/form/renderField.tsx +511 -0
  350. package/src/react/schemaRenderer/helpers.tsx +81 -0
  351. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +308 -0
  352. package/src/react/schemaRenderer/table/TableRenderer.tsx +123 -0
  353. package/src/react/schemaRenderer/table/TableRendererBody.tsx +974 -0
  354. package/src/react/schemaRenderer/table/filters.tsx +1233 -0
  355. package/src/react/schemaRenderer/table/formatCell.tsx +264 -0
  356. package/src/react/schemaRenderer/table/links.tsx +112 -0
  357. package/src/react/schemaRenderer/table/renderRowActions.tsx +52 -0
  358. package/src/react/schemaRenderer/table/url.tsx +143 -0
  359. package/src/routes/globals.ts +154 -0
  360. package/src/routes/helpers.ts +668 -0
  361. package/src/routes/pages.ts +173 -0
  362. package/src/routes/panel.ts +204 -0
  363. package/src/routes/relations.ts +1219 -0
  364. package/src/routes/resources.ts +786 -0
  365. package/src/routes/theme.ts +109 -0
  366. package/src/routes.test.ts +1 -1
  367. package/src/routes.ts +64 -3176
  368. package/src/schema/TableWidget.test.ts +2 -2
  369. package/src/theme/migrate.test.ts +178 -0
  370. package/src/vite.test.ts +184 -0
  371. package/src/vite.ts +26 -4
@@ -0,0 +1,193 @@
1
+ /**
2
+ * M2M relation Action factories — `relationAttach` (header, modal-form
3
+ * picker → POST `_action/relationAttach`), `relationDetach` (row, direct
4
+ * POST to `_detach/:childId`), `relationBulkDetach` (bulk, handler-
5
+ * dispatched).
6
+ *
7
+ * Sibling of `relationCreate / Edit / Delete` for every M2M mode
8
+ * (`belongsToMany`, `morphToMany` (owning polymorphic side),
9
+ * `morphedByMany` (inverse polymorphic side)). All three modes share
10
+ * the same `attach` / `detach` / `sync` accessor surface — the rudder
11
+ * ORM stamps + filters the polymorphic discriminator on the morph
12
+ * variants automatically, so pilotiq's pivot factories are mode-agnostic
13
+ * beyond the visibility gate.
14
+ *
15
+ * All three auto-hide outside any M2M mode so dropping a factory into
16
+ * a non-M2M manager is a no-op (visible=false) instead of a confusing
17
+ * 404.
18
+ *
19
+ * The first and third route through the manager-scoped
20
+ * `_action/:actionName` endpoint (added in routes.ts) so handlers
21
+ * see `ctx.relation = { parent, parentId, relationship }`.
22
+ *
23
+ * See `docs/plans/action-split.md` for the split plan.
24
+ */
25
+
26
+ import { Action, type ActionResult } from './Action.js'
27
+ import {
28
+ safeManagerPolicy,
29
+ type RelationManager,
30
+ type RelationManagerContext,
31
+ } from '../RelationManager.js'
32
+ import { resolveM2MAccessor } from '../orm/m2mAccessor.js'
33
+ import { buildAttachModalSchema } from './attachFactory.js'
34
+ import { isM2MMode, relationUrlPrefix } from './factoryHelpers.js'
35
+
36
+ /** Resolve the M2M accessor on `rel.parent`, null-check the requested
37
+ * method, run it, and shape the per-failure-mode error envelope.
38
+ * Used by `relationAttachAction` and `relationBulkDetachAction` —
39
+ * both follow the same pattern (resolve → null-check method →
40
+ * try/catch). Keeps the "Pivot accessor missing on …" error string
41
+ * consistent across both call sites. */
42
+ async function callM2MAccessor(
43
+ rel: { parent: unknown; relationship: string },
44
+ method: 'attach' | 'detach',
45
+ ids: string[],
46
+ failureLabel: string,
47
+ ): Promise<{ ok: true } | { ok: false; result: ActionResult }> {
48
+ const accessor = resolveM2MAccessor(rel.parent, rel.relationship) as
49
+ | { attach?: (ids: string[]) => Promise<unknown>; detach?: (ids: string[]) => Promise<unknown> }
50
+ | null
51
+ const fn = accessor?.[method]
52
+ if (typeof fn !== 'function') {
53
+ return {
54
+ ok: false,
55
+ result: { notify: { title: `Pivot accessor missing on ${rel.relationship} — wrong relation type or ORM version?`, type: 'error' } as never },
56
+ }
57
+ }
58
+ try {
59
+ await fn(ids)
60
+ return { ok: true }
61
+ } catch (err) {
62
+ return {
63
+ ok: false,
64
+ result: { notify: { title: `${failureLabel}: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never },
65
+ }
66
+ }
67
+ }
68
+
69
+ /** Header-placement attach factory — opens a modal with a SelectField
70
+ * listing related records that aren't already attached, and POSTs the
71
+ * selected id to the manager's `_action/relationAttach` endpoint.
72
+ *
73
+ * Visibility delegates to `M.canAttach(user, parentRecord)` AND
74
+ * guards against being dropped into a non-M2M manager. */
75
+ export function relationAttachAction(
76
+ M: typeof RelationManager,
77
+ ctx: RelationManagerContext,
78
+ ): Action {
79
+ const labelSingular = M.getLabelSingular()
80
+ const a = Action.make('relationAttach')
81
+ .label(`Attach ${labelSingular}`)
82
+ .header()
83
+ .modalHeading(`Attach ${labelSingular}`)
84
+ .modalSubmitLabel('Attach')
85
+ .modalCancelLabel('Cancel')
86
+ .handler(async (hctx) => {
87
+ const rel = hctx.relation
88
+ if (!rel) {
89
+ return { notify: { title: 'Attach handler missing parent context — manager-scoped _action route not wired', type: 'error' } as never }
90
+ }
91
+ const Related = ctx.related
92
+ if (!Related?.model) {
93
+ return { notify: { title: 'Cannot attach: related Resource has no model', type: 'error' } as never }
94
+ }
95
+ const idStr = String((hctx.values?.['_attachId'] as unknown) ?? '')
96
+ if (idStr.length === 0) {
97
+ return { notify: { title: 'Pick a record to attach', type: 'error' } as never }
98
+ }
99
+ const call = await callM2MAccessor(rel, 'attach', [idStr], 'Attach failed')
100
+ if (!call.ok) return call.result
101
+ return { notify: { title: `${labelSingular} attached`, type: 'success' } as never }
102
+ })
103
+ .visible(({ user }) => {
104
+ if (!isM2MMode(ctx.mode)) return false
105
+ return safeManagerPolicy(M, 'canAttach', ctx.related, user, ctx.parentRecord)
106
+ })
107
+
108
+ // Build the modal-form schema only when this is actually an M2M
109
+ // manager — non-M2M drops keep the action hidden via the visibility
110
+ // predicate, but still need a schema-less Action so the meta walker
111
+ // doesn't blow up. Static import is fine: `attachFactory` only
112
+ // depends on `SelectField` + ORM helpers, no cycle back to Action.
113
+ if (isM2MMode(ctx.mode) && ctx.related?.model) {
114
+ a.schema(buildAttachModalSchema({
115
+ Related: ctx.related,
116
+ relationship: ctx.relationship,
117
+ recordTitleAttr: M.getRecordTitleAttribute() ?? ctx.related.recordTitleAttribute,
118
+ labelSingular,
119
+ }))
120
+ }
121
+ return a
122
+ }
123
+
124
+ /** Row-placement detach factory — POSTs to
125
+ * `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/_detach`,
126
+ * destructive style with a confirmation prompt that says "Detach"
127
+ * (not "Delete") so users understand the target record stays.
128
+ * Visibility delegates to `M.canDetach`. */
129
+ export function relationDetachAction(
130
+ M: typeof RelationManager,
131
+ ctx: RelationManagerContext,
132
+ recordId?: string,
133
+ ): Action {
134
+ const id = recordId ?? ':id'
135
+ const singular = M.getLabelSingular().toLowerCase()
136
+ return Action.make('relationDetach')
137
+ .label('Detach')
138
+ .destructive()
139
+ .method('post')
140
+ .action(`${relationUrlPrefix(ctx)}/${id}/_detach`)
141
+ .confirm(`Detach this ${singular}? The ${singular} record stays in place; only the link is removed.`)
142
+ .visible(async ({ user, record }) => {
143
+ if (!isM2MMode(ctx.mode)) return false
144
+ return safeManagerPolicy(M, 'canDetach', ctx.related, user, ctx.parentRecord, record)
145
+ })
146
+ }
147
+
148
+ /** Bulk-placement bulk-detach factory — handler-dispatched. Calls
149
+ * `parent.related(rel).detach(ids)` for the selected rows. Visibility
150
+ * delegates to `M.canAttach` (acts like a "manager admin" gate; we
151
+ * intentionally don't enforce per-row `canDetach` on the visibility
152
+ * side because the bulk button needs to be visible before the user
153
+ * has selected anything — per-row gating happens inside the handler). */
154
+ export function relationBulkDetachAction(
155
+ M: typeof RelationManager,
156
+ ctx: RelationManagerContext,
157
+ ): Action {
158
+ const labelPlural = M.getLabel().toLowerCase()
159
+ return Action.make('relationBulkDetach')
160
+ .label('Detach selected')
161
+ .destructive()
162
+ .bulk()
163
+ .confirm(`Detach the selected ${labelPlural}? The records stay in place; only the links are removed.`)
164
+ .handler(async (hctx) => {
165
+ const rel = hctx.relation
166
+ if (!rel) {
167
+ return { notify: { title: 'Bulk-detach handler missing parent context — manager-scoped _action route not wired', type: 'error' } as never }
168
+ }
169
+ const records = hctx.records ?? []
170
+ // Parallelize the per-row policy probes; the accessor call itself stays a single batched op.
171
+ const allowedFlags = await Promise.all(
172
+ records.map(r => safeManagerPolicy(M, 'canDetach', ctx.related, hctx.user, ctx.parentRecord, r)),
173
+ )
174
+ const ids: string[] = []
175
+ for (let i = 0; i < records.length; i++) {
176
+ if (!allowedFlags[i]) continue
177
+ const id = String((records[i] as { id?: unknown }).id ?? '')
178
+ if (id) ids.push(id)
179
+ }
180
+ if (ids.length === 0) {
181
+ return { notify: { title: 'Nothing to detach (no permitted rows)', type: 'warning' } as never }
182
+ }
183
+ const call = await callM2MAccessor(rel, 'detach', ids, 'Bulk detach failed')
184
+ if (!call.ok) return call.result
185
+ return { notify: { title: `${ids.length} ${labelPlural} detached`, type: 'success' } as never }
186
+ })
187
+ .visible(({ user }) => {
188
+ if (!isM2MMode(ctx.mode)) return false
189
+ // Bulk gate uses canAttach as a stand-in for "manager admin" —
190
+ // per-row canDetach is enforced inside the handler.
191
+ return safeManagerPolicy(M, 'canAttach', ctx.related, user, ctx.parentRecord)
192
+ })
193
+ }
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Relation-manager Action factories — `relationCreate / relationEdit /
3
+ * relationDelete / relationRestore / relationForceDelete` (hasMany +
4
+ * morphMany) and `relationReplicate / relationBulkReplicate` (clone
5
+ * with force-pinned parent attachment).
6
+ *
7
+ * Designed to be called inside `RelationManager.static table()` — the
8
+ * page-data builder pipes `RelationManagerContext` into that
9
+ * configurator so users get `basePath`, `parentId`, and the discovered
10
+ * Related resource without threading them by hand.
11
+ *
12
+ * Visibility predicates use `safeManagerPolicy` so the manager's
13
+ * `canX` (when overridden) wins, otherwise falls through to the
14
+ * related Resource's `canX`. Throws absorb as `false`.
15
+ *
16
+ * M2M factories (`relationAttach / relationDetach / relationBulkDetach`)
17
+ * live in `m2mFactories.ts`.
18
+ *
19
+ * See `docs/plans/action-split.md` for the split plan.
20
+ */
21
+
22
+ import {
23
+ Action,
24
+ type ActionContext,
25
+ type ActionResult,
26
+ type ReplicateOptions,
27
+ type ResourceLike,
28
+ } from './Action.js'
29
+ import {
30
+ safeManagerPolicy,
31
+ type RelationManager,
32
+ type RelationManagerContext,
33
+ } from '../RelationManager.js'
34
+ import {
35
+ computeMorphPayload,
36
+ getMorphRelationDescriptor,
37
+ getParentRelationDescriptor,
38
+ type ModelLike,
39
+ } from '../orm/modelDefaults.js'
40
+ import { buildReplica, forEachAllowed, isM2MMode, isTrashed, relationUrlPrefix } from './factoryHelpers.js'
41
+
42
+ /**
43
+ * Compute the parent-attachment payload to force-pin onto a relation
44
+ * replica. For `hasMany`, returns `{ [foreignKey]: parentId }` from the
45
+ * parent's `static relations[name]` descriptor. For `morphMany` /
46
+ * `morphOne`, returns `{ <morphName>Id, <morphName>Type }` via
47
+ * `computeMorphPayload(parentRecord)`. Returns `{}` when no descriptor
48
+ * matches — the route dispatcher already auto-hides under M2M / morphTo,
49
+ * so missing descriptors there are a no-op rather than an error. Pure;
50
+ * exported for tests and re-used by both factories.
51
+ */
52
+ function computeRelationPin(
53
+ ctx: RelationManagerContext,
54
+ ): Record<string, unknown> {
55
+ const parentModel = (ctx.parentRecord as { constructor?: ModelLike } | null | undefined)?.constructor
56
+ if (!parentModel) return {}
57
+ const rel = ctx.relationship
58
+ // Polymorphic owner side first — `morphMany` carries no foreignKey
59
+ // and would fail the hasMany descriptor's gate.
60
+ if (ctx.mode === 'morphMany') {
61
+ const morph = getMorphRelationDescriptor(parentModel, rel)
62
+ if (!morph) return {}
63
+ try { return computeMorphPayload(ctx.parentRecord, morph) }
64
+ catch { return {} }
65
+ }
66
+ const desc = getParentRelationDescriptor(parentModel, rel)
67
+ if (!desc) return {}
68
+ return { [desc.foreignKey]: ctx.parentId }
69
+ }
70
+
71
+ /**
72
+ * Build + persist a single relation replica. Runs the strip set
73
+ * (PK + soft-delete column on the **related** Resource +
74
+ * `opts.excludeAttributes`), force-pins the parent attachment columns,
75
+ * runs the optional `beforeReplicaSaved` hook, and calls
76
+ * `Related.model.create(...)`. Returns the model's create result so
77
+ * callers can read its primary key for redirect targeting.
78
+ *
79
+ * Throws when the related Resource has no model — caller (single-row
80
+ * factory) catches and surfaces an error notification; bulk caller
81
+ * checks the model presence ahead of the loop.
82
+ */
83
+ async function persistRelationReplica(
84
+ _M: typeof RelationManager,
85
+ ctx: RelationManagerContext,
86
+ source: unknown,
87
+ opts: ReplicateOptions,
88
+ ): Promise<unknown> {
89
+ const Related = ctx.related
90
+ if (!Related?.model || typeof Related.model.create !== 'function') {
91
+ throw new Error('Related Resource has no model.create')
92
+ }
93
+ const M2 = Related.model as ModelLike
94
+ // Force-pin the parent attachment via `pin` so `beforeReplicaSaved` can
95
+ // still read / override the FK if it really wants to (rare); tampered
96
+ // source rows can't slip a different parent in by riding their own FK
97
+ // column — the pin overwrites whatever value was there.
98
+ const { replica } = await buildReplica(source, M2, {
99
+ excludeAttributes: opts.excludeAttributes,
100
+ deletedAtColumn: Related.deletedAtColumn,
101
+ pin: computeRelationPin(ctx),
102
+ beforeReplicaSaved: opts.beforeReplicaSaved,
103
+ })
104
+ return M2.create(replica)
105
+ }
106
+
107
+ /**
108
+ * Single-row dispatch for `relationReplicateAction`. Resolves
109
+ * `ctx.record` (loaded by the route's resolveRecord hook), validates,
110
+ * persists the replica, and shapes the success notification. Errors
111
+ * are caught and surfaced as error toasts.
112
+ */
113
+ async function runRelationReplicateRow(
114
+ M: typeof RelationManager,
115
+ ctx: RelationManagerContext,
116
+ hctx: ActionContext,
117
+ opts: ReplicateOptions,
118
+ ): Promise<ActionResult> {
119
+ const source = hctx.record
120
+ if (!source || typeof source !== 'object') {
121
+ return { notify: { title: 'Replicate failed: source record missing', type: 'error' } as never }
122
+ }
123
+ const Related = ctx.related
124
+ if (!Related?.model || typeof Related.model.create !== 'function') {
125
+ return { notify: { title: 'Replicate not configured (related Resource has no model.create)', type: 'error' } as never }
126
+ }
127
+ let created: unknown
128
+ try {
129
+ created = await persistRelationReplica(M, ctx, source, opts)
130
+ } catch (err) {
131
+ return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
132
+ }
133
+ const overrideTitle = opts.getCreatedNotificationTitle
134
+ ? await opts.getCreatedNotificationTitle({ replica: created, source })
135
+ : undefined
136
+ const title = overrideTitle !== undefined ? overrideTitle : `${M.getLabelSingular()} replicated`
137
+ // The manager-scoped `_action/:actionName` route falls back to the
138
+ // manager list URL when `result.redirect` is undefined, so we only
139
+ // emit `redirect` when the user override returned a string. That
140
+ // way default behavior (route owns the fallback) is unchanged.
141
+ const overrideRedirect = opts.getRedirectUrl
142
+ ? await opts.getRedirectUrl({ replica: created, source })
143
+ : undefined
144
+ return {
145
+ ...(overrideRedirect !== undefined ? { redirect: overrideRedirect } : {}),
146
+ notify: { title, type: 'success' } as never,
147
+ }
148
+ }
149
+
150
+ /** Relation create-action factory — link to
151
+ * `${base}/${parentSlug}/${parentId}/${relationship}/create`.
152
+ *
153
+ * Visibility delegates to `M.canCreate(user, parentRecord)` (or the
154
+ * related Resource's `canCreate(user)` when the manager hasn't
155
+ * overridden). Drop into `headerActions([...])` from inside
156
+ * `RelationManager.table(table, ctx)`.
157
+ */
158
+ export function relationCreateAction(
159
+ M: typeof RelationManager,
160
+ ctx: RelationManagerContext,
161
+ ): Action {
162
+ const labelSingular = M.getLabelSingular()
163
+ return Action.make('create')
164
+ .label(`New ${labelSingular}`)
165
+ .href(`${relationUrlPrefix(ctx)}/create`)
166
+ .visible(({ user }) => {
167
+ // M2M managers don't have a per-pivot-row create surface — the
168
+ // related record is created via its own Resource, then attached
169
+ // via `relationAttach`. Auto-hide so dropping this factory into
170
+ // any M2M manager (belongsToMany / morphToMany / morphedByMany)
171
+ // is a no-op (visible=false) instead of a 404-on-click foot-gun.
172
+ if (isM2MMode(ctx.mode)) return false
173
+ return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
174
+ })
175
+ }
176
+
177
+ /** Relation edit-action factory — link to
178
+ * `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/edit`.
179
+ *
180
+ * Same `recordId` semantics as `editAction`: omit for row context
181
+ * so the renderer substitutes `:id` per row; pass explicitly when
182
+ * building actions for a single-record context. Visibility delegates
183
+ * to `M.canEdit(user, child, parentRecord)` with fall-through to the
184
+ * related Resource's `canEdit(user, record)`.
185
+ */
186
+ export function relationEditAction(
187
+ M: typeof RelationManager,
188
+ ctx: RelationManagerContext,
189
+ recordId?: string,
190
+ ): Action {
191
+ const id = recordId ?? ':id'
192
+ return Action.make('edit')
193
+ .label('Edit')
194
+ .href(`${relationUrlPrefix(ctx)}/${id}/edit`)
195
+ .visible(({ user, record }) => {
196
+ // M2M: per-pivot-row "edit" doesn't exist; users edit the
197
+ // related record via its own Resource. Auto-hide for every M2M
198
+ // mode (belongsToMany / morphToMany / morphedByMany).
199
+ if (isM2MMode(ctx.mode)) return false
200
+ return safeManagerPolicy(M, 'canEdit', ctx.related, user, ctx.parentRecord, record)
201
+ })
202
+ }
203
+
204
+ /** Relation delete-action factory — POST to
205
+ * `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/delete`,
206
+ * destructive style with a labeled confirmation. Visibility delegates
207
+ * to `M.canDelete(user, child, parentRecord)` with fall-through to the
208
+ * related Resource's `canDelete(user, record)`.
209
+ */
210
+ export function relationDeleteAction(
211
+ M: typeof RelationManager,
212
+ ctx: RelationManagerContext,
213
+ recordId?: string,
214
+ ): Action {
215
+ const id = recordId ?? ':id'
216
+ const singular = M.getLabelSingular().toLowerCase()
217
+ return Action.make('delete')
218
+ .label('Delete')
219
+ .destructive()
220
+ .method('post')
221
+ .action(`${relationUrlPrefix(ctx)}/${id}/delete`)
222
+ .confirm(`Delete this ${singular}?`)
223
+ .visible(async ({ user, record }) => {
224
+ // M2M: "delete" of the related record is destructive in a way
225
+ // that "detach" isn't — surface only `relationDetach` on every
226
+ // M2M manager (belongsToMany / morphToMany / morphedByMany).
227
+ // Users who genuinely want to delete the related record reach
228
+ // for `Action.delete(R)` on the related Resource instead.
229
+ if (isM2MMode(ctx.mode)) return false
230
+ if (ctx.related?.softDeletes && isTrashed(record, ctx.related as ResourceLike)) return false
231
+ return safeManagerPolicy(M, 'canDelete', ctx.related, user, ctx.parentRecord, record)
232
+ })
233
+ }
234
+
235
+ /**
236
+ * Plan #13 polish — Restore factory for relation managers. POSTs to
237
+ * `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/restore`,
238
+ * success-styled, no confirm prompt. Auto-hides on live (non-trashed)
239
+ * rows AND when `M.canRestore` (or related Resource fall-through)
240
+ * denies. Drop into `recordActions([...])` from `RelationManager.table(table, ctx)`.
241
+ */
242
+ export function relationRestoreAction(
243
+ M: typeof RelationManager,
244
+ ctx: RelationManagerContext,
245
+ recordId?: string,
246
+ ): Action {
247
+ const id = recordId ?? ':id'
248
+ return Action.make('restore')
249
+ .label('Restore')
250
+ .color('success')
251
+ .method('post')
252
+ .action(`${relationUrlPrefix(ctx)}/${id}/restore`)
253
+ .visible(async ({ user, record }) => {
254
+ if (!ctx.related?.softDeletes) return false
255
+ if (!isTrashed(record, ctx.related as ResourceLike)) return false
256
+ return safeManagerPolicy(M, 'canRestore', ctx.related, user, ctx.parentRecord, record)
257
+ })
258
+ }
259
+
260
+ /**
261
+ * Plan #13 polish — Force-delete factory for relation managers. POSTs
262
+ * to `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/force-delete`,
263
+ * destructive style with a permanence-aware confirmation. Auto-hides on
264
+ * live (non-trashed) rows and when policy denies.
265
+ */
266
+ export function relationForceDeleteAction(
267
+ M: typeof RelationManager,
268
+ ctx: RelationManagerContext,
269
+ recordId?: string,
270
+ ): Action {
271
+ const id = recordId ?? ':id'
272
+ const singular = M.getLabelSingular().toLowerCase()
273
+ return Action.make('forceDelete')
274
+ .label('Delete forever')
275
+ .destructive()
276
+ .method('post')
277
+ .action(`${relationUrlPrefix(ctx)}/${id}/force-delete`)
278
+ .confirm(`Permanently delete this ${singular}? This cannot be undone.`)
279
+ .visible(async ({ user, record }) => {
280
+ if (!ctx.related?.softDeletes) return false
281
+ if (!isTrashed(record, ctx.related as ResourceLike)) return false
282
+ return safeManagerPolicy(M, 'canForceDelete', ctx.related, user, ctx.parentRecord, record)
283
+ })
284
+ }
285
+
286
+ /**
287
+ * Relation row-replicate factory. Clones the row's child record
288
+ * inside the manager's parent scope.
289
+ *
290
+ * Strips the related model's primary key, soft-delete column, and
291
+ * `opts.excludeAttributes`. Re-applies the parent attachment columns
292
+ * after the strip + before the optional `beforeReplicaSaved` hook,
293
+ * so user code can still mutate non-FK fields without accidentally
294
+ * unlinking the replica.
295
+ *
296
+ * On success the manager-scoped route falls back to the manager
297
+ * list URL (`${base}/${parentSlug}/${parentId}/${relationship}`)
298
+ * because no explicit `redirect` is returned — same default as the
299
+ * other handler-style relation factories.
300
+ *
301
+ * `recordId` kept in the signature for parity with the rest of the
302
+ * relation factory family. The dispatcher resolves the source row
303
+ * from the request body, so it isn't referenced here.
304
+ */
305
+ export function relationReplicateAction(
306
+ M: typeof RelationManager,
307
+ ctx: RelationManagerContext,
308
+ recordId?: string,
309
+ opts: ReplicateOptions = {},
310
+ ): Action {
311
+ void recordId
312
+ return Action.make('relationReplicate')
313
+ .label('Replicate')
314
+ .row()
315
+ .handler(async (hctx) => {
316
+ const result = await runRelationReplicateRow(M, ctx, hctx, opts)
317
+ return result
318
+ })
319
+ .visible(({ user }) => {
320
+ if (isM2MMode(ctx.mode) || ctx.mode === 'morphTo') return false
321
+ return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
322
+ })
323
+ }
324
+
325
+ /**
326
+ * Bulk sibling — replicates every selected child row inside the
327
+ * manager's parent scope. Same strip + force-pin pipeline applied
328
+ * per row. Per-row `safeManagerPolicy(M, 'canCreate', …)` runs
329
+ * inside the loop so a partially-permitted selection still proceeds
330
+ * for the rows that pass. Rows that throw are skipped silently —
331
+ * the toast count reflects only successful creates.
332
+ */
333
+ export function relationBulkReplicateAction(
334
+ M: typeof RelationManager,
335
+ ctx: RelationManagerContext,
336
+ opts: ReplicateOptions = {},
337
+ ): Action {
338
+ const labelPlural = M.getLabel().toLowerCase()
339
+ const labelSingular = M.getLabelSingular().toLowerCase()
340
+ return Action.make('relationBulkReplicate')
341
+ .label('Replicate selected')
342
+ .bulk()
343
+ .confirm(`Replicate the selected ${labelPlural}?`)
344
+ .handler(async (hctx) => {
345
+ const Related = ctx.related
346
+ if (!Related?.model || typeof Related.model.create !== 'function') {
347
+ return { notify: { title: 'Replicate not configured (related Resource has no model.create)', type: 'error' } as never }
348
+ }
349
+ const records = hctx.records ?? []
350
+ // Per-row predicate eval preserved — users may write stateful predicates that gate per-attempt.
351
+ const n = await forEachAllowed(
352
+ records,
353
+ () => safeManagerPolicy(M, 'canCreate', Related, hctx.user, ctx.parentRecord),
354
+ async (_id, source) => { await persistRelationReplica(M, ctx, source, opts) },
355
+ )
356
+ if (n === 0) {
357
+ return { notify: { title: `Nothing to replicate (no permitted rows)`, type: 'warning' } as never }
358
+ }
359
+ const defaultTitle = `${n} ${n === 1 ? labelSingular : labelPlural} replicated`
360
+ const overrideTitle = opts.getCreatedNotificationTitle
361
+ ? await opts.getCreatedNotificationTitle({ count: n, records })
362
+ : undefined
363
+ const title = overrideTitle !== undefined ? overrideTitle : defaultTitle
364
+ return {
365
+ notify: { title, type: 'success' } as never,
366
+ }
367
+ })
368
+ .visible(({ user }) => {
369
+ if (isM2MMode(ctx.mode) || ctx.mode === 'morphTo') return false
370
+ return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
371
+ })
372
+ }
@@ -1646,7 +1646,7 @@ async function persistRelationshipRows(
1646
1646
  keptPks.add(submittedId!)
1647
1647
  if (afterUpdate) await afterUpdate(updatedRecord, buildRowCtx(idx))
1648
1648
  } else {
1649
- let createdRecord: unknown = undefined
1649
+ let createdRecord: unknown
1650
1650
  if (attachment.kind === 'hasMany') {
1651
1651
  payload[attachment.foreignKey] = parentPk
1652
1652
  createdRecord = await model.create(payload)
@@ -568,7 +568,7 @@ export async function loadTableRecords(
568
568
  // ONE auth call per row regardless of editable column count
569
569
  // — same record, same answer. Failures or false → no edit
570
570
  // affordance for any column.
571
- let allowed = false
571
+ let allowed: boolean
572
572
  try { allowed = await hooks!.canEdit!(user, recordObj) }
573
573
  catch { allowed = false }
574
574
  if (allowed) {
@@ -111,6 +111,18 @@ export interface FieldMeta extends ElementMeta {
111
111
  * `Column.formatStateUsing` from Plan #2).
112
112
  */
113
113
  formattedValue?: string
114
+ /**
115
+ * Per-field collab override. Absent = inherit the panel-wide default
116
+ * (collab on every record-edit page when `@pilotiq-pro/collab` is
117
+ * registered). Explicit `false` opts THIS field out of the collab
118
+ * layer entirely — no value sync AND no presence chip — so the field
119
+ * stays device-local inside an otherwise collab-on form. Useful for
120
+ * sensitive scratch space or fields whose LWW semantics would
121
+ * surprise users (rapid typing in plain-text inputs). Forward-compat
122
+ * for `true` (opt in even when panel default is off) once
123
+ * panel-level disable lands.
124
+ */
125
+ collab?: boolean
114
126
  }
115
127
 
116
128
  /**
@@ -278,6 +290,10 @@ export abstract class Field extends Element {
278
290
  protected _hiddenOn?: ReadonlyArray<'table' | 'create' | 'edit' | 'view'>
279
291
  protected _visibleOn?: ReadonlyArray<'table' | 'create' | 'edit' | 'view'>
280
292
 
293
+ // Per-field collab override. `undefined` = inherit panel default;
294
+ // explicit `false` = opt out of value sync AND presence entirely.
295
+ protected _collab?: boolean
296
+
281
297
  constructor(name: string, type: FieldType) {
282
298
  super()
283
299
  this.name = name
@@ -477,6 +493,28 @@ export abstract class Field extends Element {
477
493
  */
478
494
  dehydrated(value: boolean = true): this { this._dehydrated = value; return this }
479
495
 
496
+ /**
497
+ * Per-field realtime-collab override.
498
+ *
499
+ * - `.collab(false)` — opts THIS field out of the collab layer
500
+ * entirely (no value sync via the form-level CRDT, no presence
501
+ * chip rendered next to the label). The field stays device-local
502
+ * inside an otherwise collab-on form. Useful for private scratch
503
+ * space or fields whose LWW semantics would surprise users
504
+ * (rapid typing in plain-text inputs hit the v1 last-writer-wins
505
+ * footgun — `.collab(false)` is the v1 escape hatch).
506
+ * - `.collab(true)` (default of the bare call) — explicit opt-in.
507
+ * Forward-compat for a future panel-level disable; today it
508
+ * behaves the same as not calling the setter when collab is
509
+ * already on at the panel level.
510
+ * - Not calling the setter — inherits the panel-wide default
511
+ * (collab on whenever `.plugins([collab()])` is registered).
512
+ */
513
+ collab(enabled = true): this {
514
+ this._collab = enabled
515
+ return this
516
+ }
517
+
480
518
  /**
481
519
  * Display-time transform — receives `(value, { record })` and returns
482
520
  * a string. Result lands on `FieldMeta.formattedValue`; renderers
@@ -769,6 +807,7 @@ export abstract class Field extends Element {
769
807
  ...(this._extraAttributes !== undefined ? { extraAttributes: this._extraAttributes } : {}),
770
808
  ...(this._extraInputAttributes !== undefined ? { extraInputAttributes: this._extraInputAttributes } : {}),
771
809
  ...(this._extraFieldWrapperAttributes !== undefined ? { extraFieldWrapperAttributes: this._extraFieldWrapperAttributes } : {}),
810
+ ...(this._collab !== undefined ? { collab: this._collab } : {}),
772
811
  }
773
812
  }
774
813