@pilotiq/pilotiq 0.23.1 → 0.24.2

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 (500) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/boost/guidelines.md +566 -0
  3. package/boost/skills/pilotiq-fields/SKILL.md +47 -0
  4. package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
  5. package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
  6. package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
  7. package/boost/skills/pilotiq-relations/SKILL.md +47 -0
  8. package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
  9. package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
  10. package/boost/skills/pilotiq-resource/SKILL.md +61 -0
  11. package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
  12. package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
  13. package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
  14. package/dist/actions/exportFactory.d.ts +10 -0
  15. package/dist/actions/exportFactory.d.ts.map +1 -1
  16. package/dist/actions/exportFactory.js +10 -0
  17. package/dist/actions/exportFactory.js.map +1 -1
  18. package/dist/react/CollabRoomContext.d.ts +5 -5
  19. package/dist/react/index.d.ts +0 -1
  20. package/dist/react/index.d.ts.map +1 -1
  21. package/dist/react/index.js +0 -1
  22. package/dist/react/index.js.map +1 -1
  23. package/dist/routes/helpers.d.ts.map +1 -1
  24. package/dist/routes/helpers.js +6 -2
  25. package/dist/routes/helpers.js.map +1 -1
  26. package/dist/routes/relations.d.ts.map +1 -1
  27. package/dist/routes/relations.js +12 -0
  28. package/dist/routes/relations.js.map +1 -1
  29. package/package.json +6 -1
  30. package/.turbo/turbo-build.log +0 -8
  31. package/CLAUDE.md +0 -265
  32. package/dist/react/useCollabSeed.d.ts +0 -23
  33. package/dist/react/useCollabSeed.d.ts.map +0 -1
  34. package/dist/react/useCollabSeed.js +0 -82
  35. package/dist/react/useCollabSeed.js.map +0 -1
  36. package/src/Cluster.test.ts +0 -283
  37. package/src/Cluster.ts +0 -83
  38. package/src/Column.test.ts +0 -199
  39. package/src/Column.ts +0 -710
  40. package/src/Global.test.ts +0 -367
  41. package/src/Global.ts +0 -169
  42. package/src/Page.test.ts +0 -114
  43. package/src/Page.ts +0 -208
  44. package/src/Pilotiq.perf.test.ts +0 -252
  45. package/src/Pilotiq.test.ts +0 -129
  46. package/src/Pilotiq.ts +0 -1158
  47. package/src/PilotiqRegistry.ts +0 -36
  48. package/src/PilotiqServiceProvider.ts +0 -121
  49. package/src/RelationManager.test.ts +0 -400
  50. package/src/RelationManager.ts +0 -527
  51. package/src/RenderHook.test.ts +0 -252
  52. package/src/RenderHook.ts +0 -242
  53. package/src/Resource.test.ts +0 -284
  54. package/src/Resource.ts +0 -526
  55. package/src/RightPanel.test.ts +0 -202
  56. package/src/RightPanel.ts +0 -132
  57. package/src/Tab.test.ts +0 -91
  58. package/src/Tab.ts +0 -156
  59. package/src/UserMenuItem.ts +0 -145
  60. package/src/actions/Action.test.ts +0 -2526
  61. package/src/actions/Action.ts +0 -1515
  62. package/src/actions/ActionGroup.test.ts +0 -112
  63. package/src/actions/ActionGroup.ts +0 -173
  64. package/src/actions/attachFactory.ts +0 -172
  65. package/src/actions/bulkFactories.ts +0 -168
  66. package/src/actions/crudFactories.ts +0 -220
  67. package/src/actions/exportFactory.ts +0 -215
  68. package/src/actions/factoryHelpers.ts +0 -177
  69. package/src/actions/importFactory.ts +0 -243
  70. package/src/actions/index.ts +0 -17
  71. package/src/actions/m2mFactories.ts +0 -193
  72. package/src/actions/relationFactories.ts +0 -372
  73. package/src/applyPageHooks.test.ts +0 -463
  74. package/src/applyPageHooks.ts +0 -330
  75. package/src/authorization.test.ts +0 -483
  76. package/src/breadcrumbs.test.ts +0 -238
  77. package/src/cells/coerce.test.ts +0 -85
  78. package/src/cells/coerce.ts +0 -84
  79. package/src/clusterPaths.ts +0 -35
  80. package/src/columns/BadgeColumn.test.ts +0 -54
  81. package/src/columns/BadgeColumn.ts +0 -32
  82. package/src/columns/BooleanColumn.test.ts +0 -41
  83. package/src/columns/BooleanColumn.ts +0 -18
  84. package/src/columns/ColorColumn.test.ts +0 -37
  85. package/src/columns/ColorColumn.ts +0 -38
  86. package/src/columns/IconColumn.test.ts +0 -54
  87. package/src/columns/IconColumn.ts +0 -37
  88. package/src/columns/ImageColumn.test.ts +0 -41
  89. package/src/columns/ImageColumn.ts +0 -28
  90. package/src/columns/SelectColumn.ts +0 -98
  91. package/src/columns/TextColumn.test.ts +0 -190
  92. package/src/columns/TextColumn.ts +0 -20
  93. package/src/columns/TextInputColumn.ts +0 -68
  94. package/src/columns/ToggleColumn.ts +0 -46
  95. package/src/columns/editableColumns.test.ts +0 -238
  96. package/src/columns/index.ts +0 -9
  97. package/src/defaultGlobalPages.ts +0 -95
  98. package/src/defaultPages.test.ts +0 -634
  99. package/src/defaultPages.ts +0 -617
  100. package/src/defaultViewPage.test.ts +0 -147
  101. package/src/elements/Form.test.ts +0 -223
  102. package/src/elements/Form.ts +0 -416
  103. package/src/elements/ListTabs.ts +0 -28
  104. package/src/elements/Table.test.ts +0 -422
  105. package/src/elements/Table.ts +0 -850
  106. package/src/elements/TableGroup.test.ts +0 -260
  107. package/src/elements/TableGroup.ts +0 -334
  108. package/src/elements/dispatchAction.test.ts +0 -463
  109. package/src/elements/dispatchAction.ts +0 -355
  110. package/src/elements/dispatchForm.test.ts +0 -477
  111. package/src/elements/dispatchForm.ts +0 -1993
  112. package/src/elements/dispatchTable.test.ts +0 -1514
  113. package/src/elements/dispatchTable.ts +0 -745
  114. package/src/elements/index.ts +0 -21
  115. package/src/entries/BadgeEntry.ts +0 -39
  116. package/src/entries/CodeEntry.test.ts +0 -40
  117. package/src/entries/CodeEntry.ts +0 -52
  118. package/src/entries/ColorEntry.ts +0 -63
  119. package/src/entries/ComponentEntry.test.ts +0 -173
  120. package/src/entries/ComponentEntry.ts +0 -95
  121. package/src/entries/Entry.ts +0 -304
  122. package/src/entries/IconEntry.ts +0 -49
  123. package/src/entries/ImageEntry.ts +0 -61
  124. package/src/entries/KeyValueEntry.ts +0 -47
  125. package/src/entries/RepeatableEntry.test.ts +0 -239
  126. package/src/entries/RepeatableEntry.ts +0 -173
  127. package/src/entries/TextEntry.test.ts +0 -394
  128. package/src/entries/TextEntry.ts +0 -60
  129. package/src/entries/index.ts +0 -12
  130. package/src/entries/leaves.test.ts +0 -306
  131. package/src/entries/registry.ts +0 -54
  132. package/src/fields/BuilderField.test.ts +0 -1188
  133. package/src/fields/BuilderField.ts +0 -605
  134. package/src/fields/BuilderRelationship.test.ts +0 -811
  135. package/src/fields/CheckboxField.test.ts +0 -44
  136. package/src/fields/CheckboxField.ts +0 -27
  137. package/src/fields/CheckboxListField.test.ts +0 -99
  138. package/src/fields/CheckboxListField.ts +0 -66
  139. package/src/fields/ColorPickerField.test.ts +0 -33
  140. package/src/fields/ColorPickerField.ts +0 -25
  141. package/src/fields/DateField.ts +0 -54
  142. package/src/fields/DateTimeField.test.ts +0 -55
  143. package/src/fields/EmailField.ts +0 -16
  144. package/src/fields/Field.test.ts +0 -654
  145. package/src/fields/Field.ts +0 -817
  146. package/src/fields/FileUploadField.test.ts +0 -143
  147. package/src/fields/FileUploadField.ts +0 -159
  148. package/src/fields/HiddenField.test.ts +0 -27
  149. package/src/fields/HiddenField.ts +0 -28
  150. package/src/fields/KeyValueField.test.ts +0 -105
  151. package/src/fields/KeyValueField.ts +0 -55
  152. package/src/fields/MarkdownField.test.ts +0 -167
  153. package/src/fields/MarkdownField.ts +0 -162
  154. package/src/fields/NumberField.ts +0 -33
  155. package/src/fields/RadioField.test.ts +0 -94
  156. package/src/fields/RadioField.ts +0 -67
  157. package/src/fields/RepeaterField.test.ts +0 -1806
  158. package/src/fields/RepeaterField.ts +0 -939
  159. package/src/fields/RepeaterRelationship.test.ts +0 -1923
  160. package/src/fields/RepeaterSimple.test.ts +0 -248
  161. package/src/fields/RowButton.test.ts +0 -219
  162. package/src/fields/RowButton.ts +0 -135
  163. package/src/fields/SelectField.test.ts +0 -192
  164. package/src/fields/SelectField.ts +0 -235
  165. package/src/fields/SliderField.test.ts +0 -50
  166. package/src/fields/SliderField.ts +0 -53
  167. package/src/fields/SlugField.ts +0 -24
  168. package/src/fields/TagsInputField.test.ts +0 -154
  169. package/src/fields/TagsInputField.ts +0 -133
  170. package/src/fields/TextField.test.ts +0 -213
  171. package/src/fields/TextField.ts +0 -177
  172. package/src/fields/TextareaField.test.ts +0 -58
  173. package/src/fields/TextareaField.ts +0 -59
  174. package/src/fields/ToggleButtonsField.test.ts +0 -106
  175. package/src/fields/ToggleButtonsField.ts +0 -59
  176. package/src/fields/ToggleField.ts +0 -16
  177. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
  178. package/src/fields/optionsResolver.ts +0 -95
  179. package/src/fields/resolveField.ts +0 -28
  180. package/src/filters/BooleanFilter.ts +0 -35
  181. package/src/filters/DateRangeFilter.test.ts +0 -194
  182. package/src/filters/DateRangeFilter.ts +0 -148
  183. package/src/filters/Filter.test.ts +0 -268
  184. package/src/filters/Filter.ts +0 -184
  185. package/src/filters/FormFilter.test.ts +0 -238
  186. package/src/filters/FormFilter.ts +0 -215
  187. package/src/filters/MultiSelectFilter.test.ts +0 -119
  188. package/src/filters/MultiSelectFilter.ts +0 -78
  189. package/src/filters/QueryBuilderFilter.test.ts +0 -662
  190. package/src/filters/QueryBuilderFilter.ts +0 -398
  191. package/src/filters/SelectFilter.ts +0 -46
  192. package/src/filters/TernaryFilter.test.ts +0 -160
  193. package/src/filters/TernaryFilter.ts +0 -72
  194. package/src/filters/TrashedFilter.test.ts +0 -149
  195. package/src/filters/TrashedFilter.ts +0 -55
  196. package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
  197. package/src/filters/queryBuilder/Constraint.ts +0 -115
  198. package/src/filters/queryBuilder/DateConstraint.ts +0 -69
  199. package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
  200. package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
  201. package/src/filters/queryBuilder/TextConstraint.ts +0 -64
  202. package/src/filters/queryBuilder/index.ts +0 -12
  203. package/src/icons/index.ts +0 -2
  204. package/src/icons/lucide.ts +0 -204
  205. package/src/icons/registry.test.ts +0 -56
  206. package/src/icons/registry.ts +0 -41
  207. package/src/icons/types.ts +0 -47
  208. package/src/index.ts +0 -525
  209. package/src/io/csv.test.ts +0 -142
  210. package/src/io/csv.ts +0 -170
  211. package/src/nestedRelationManagerData.test.ts +0 -547
  212. package/src/notifications/Notification.test.ts +0 -210
  213. package/src/notifications/Notification.ts +0 -354
  214. package/src/notifications/broadcast.test.ts +0 -110
  215. package/src/notifications/broadcast.ts +0 -95
  216. package/src/notifications/database.test.ts +0 -383
  217. package/src/notifications/database.ts +0 -398
  218. package/src/notifications/databaseNotifications.test.ts +0 -187
  219. package/src/notifications/dispatchNotificationAction.test.ts +0 -341
  220. package/src/notifications/dispatchNotificationAction.ts +0 -142
  221. package/src/notifications/flash.test.ts +0 -89
  222. package/src/notifications/flash.ts +0 -71
  223. package/src/notifications/index.ts +0 -45
  224. package/src/notifications/registerBroadcastAuth.test.ts +0 -134
  225. package/src/notifications/registerBroadcastAuth.ts +0 -100
  226. package/src/notifications/resolveSavedNotification.test.ts +0 -82
  227. package/src/notifications/resolveSavedNotification.ts +0 -59
  228. package/src/notifications/types.ts +0 -93
  229. package/src/orm/m2mAccessor.ts +0 -66
  230. package/src/orm/modelDefaults.test.ts +0 -633
  231. package/src/orm/modelDefaults.ts +0 -666
  232. package/src/pageData/breadcrumbs.ts +0 -288
  233. package/src/pageData/forms.ts +0 -578
  234. package/src/pageData/helpers.ts +0 -857
  235. package/src/pageData/misc.ts +0 -347
  236. package/src/pageData/navigation.ts +0 -842
  237. package/src/pageData/relationPages.ts +0 -1248
  238. package/src/pageData/relationTabs.ts +0 -286
  239. package/src/pageData/resourcePages.ts +0 -609
  240. package/src/pageData.test.ts +0 -1545
  241. package/src/pageData.ts +0 -341
  242. package/src/plugins/index.ts +0 -8
  243. package/src/plugins/themeEditor.test.ts +0 -36
  244. package/src/plugins/themeEditor.ts +0 -45
  245. package/src/react/AppShell.tsx +0 -251
  246. package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
  247. package/src/react/CollabRoomContext.ts +0 -98
  248. package/src/react/CollabTextRendererRegistry.ts +0 -102
  249. package/src/react/CommandPalette.tsx +0 -375
  250. package/src/react/CurrentUserContext.tsx +0 -50
  251. package/src/react/CustomPageWrapperGate.tsx +0 -69
  252. package/src/react/CustomPageWrapperRegistry.ts +0 -45
  253. package/src/react/FieldFocusReporterRegistry.ts +0 -37
  254. package/src/react/FieldLabelSlotRegistry.ts +0 -30
  255. package/src/react/FieldPresenceRegistry.ts +0 -46
  256. package/src/react/FormCollabBindingRegistry.ts +0 -242
  257. package/src/react/FormStateContext.tsx +0 -591
  258. package/src/react/HeadHooks.tsx +0 -126
  259. package/src/react/MarkdownEditorRegistry.test.ts +0 -38
  260. package/src/react/MarkdownEditorRegistry.ts +0 -107
  261. package/src/react/NotificationActionStrip.tsx +0 -263
  262. package/src/react/NotificationBell.tsx +0 -426
  263. package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
  264. package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
  265. package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
  266. package/src/react/PendingSuggestionsContext.tsx +0 -172
  267. package/src/react/RecordWrapperGate.tsx +0 -58
  268. package/src/react/RecordWrapperRegistry.ts +0 -39
  269. package/src/react/RenderHookSlot.tsx +0 -32
  270. package/src/react/RightSidebar.tsx +0 -257
  271. package/src/react/RightSidebarContext.tsx +0 -234
  272. package/src/react/RightSidebarTrigger.tsx +0 -53
  273. package/src/react/RowCoordsContext.tsx +0 -23
  274. package/src/react/SchemaRenderer.tsx +0 -549
  275. package/src/react/SearchTrigger.tsx +0 -46
  276. package/src/react/ThemeProvider.tsx +0 -93
  277. package/src/react/ThemeSettingsPage.tsx +0 -579
  278. package/src/react/ThemeToggle.tsx +0 -20
  279. package/src/react/Toaster.tsx +0 -158
  280. package/src/react/UserMenu.tsx +0 -196
  281. package/src/react/WidgetDataContext.tsx +0 -157
  282. package/src/react/cells/EditableCell.tsx +0 -389
  283. package/src/react/component-slots.test.ts +0 -103
  284. package/src/react/component-slots.ts +0 -116
  285. package/src/react/fieldJsHandler.test.ts +0 -166
  286. package/src/react/fieldJsHandler.ts +0 -79
  287. package/src/react/fields/BuilderInput.tsx +0 -1078
  288. package/src/react/fields/CheckboxInput.tsx +0 -39
  289. package/src/react/fields/CheckboxListInput.tsx +0 -102
  290. package/src/react/fields/ColorInput.tsx +0 -71
  291. package/src/react/fields/DateFieldInput.tsx +0 -70
  292. package/src/react/fields/DateTimeInput.tsx +0 -62
  293. package/src/react/fields/FieldShell.tsx +0 -348
  294. package/src/react/fields/FileUploadInput.tsx +0 -639
  295. package/src/react/fields/HiddenInput.tsx +0 -17
  296. package/src/react/fields/KeyValueInput.tsx +0 -230
  297. package/src/react/fields/MarkdownInput.tsx +0 -560
  298. package/src/react/fields/RadioInput.tsx +0 -81
  299. package/src/react/fields/RepeaterInput.test.ts +0 -116
  300. package/src/react/fields/RepeaterInput.tsx +0 -1420
  301. package/src/react/fields/SelectFieldInput.tsx +0 -280
  302. package/src/react/fields/SliderInput.tsx +0 -81
  303. package/src/react/fields/TagsInput.tsx +0 -283
  304. package/src/react/fields/TextLikeInput.tsx +0 -256
  305. package/src/react/fields/ToggleButtonsInput.tsx +0 -60
  306. package/src/react/fields/ToggleFieldInput.tsx +0 -56
  307. package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
  308. package/src/react/fields/relationshipRenameDispatch.ts +0 -97
  309. package/src/react/fields/repeaterReconcile.test.ts +0 -114
  310. package/src/react/fields/repeaterReconcile.ts +0 -104
  311. package/src/react/fields/rowChromeButton.tsx +0 -336
  312. package/src/react/fields/rowState.ts +0 -106
  313. package/src/react/fields/syncRowGates.test.ts +0 -202
  314. package/src/react/fields/syncRowGates.ts +0 -66
  315. package/src/react/fields/textInputControls.tsx +0 -238
  316. package/src/react/fields/useRowReorderDnd.ts +0 -78
  317. package/src/react/formStateHelpers.test.ts +0 -508
  318. package/src/react/formStateHelpers.ts +0 -381
  319. package/src/react/hooks/use-mobile.ts +0 -19
  320. package/src/react/icon-context.tsx +0 -60
  321. package/src/react/index.ts +0 -195
  322. package/src/react/layouts/SidebarLayout.tsx +0 -250
  323. package/src/react/layouts/TopbarLayout.tsx +0 -258
  324. package/src/react/navigate.tsx +0 -37
  325. package/src/react/onProviderSynced.test.ts +0 -90
  326. package/src/react/parseRecordEditUrl.test.ts +0 -122
  327. package/src/react/parseRecordEditUrl.ts +0 -94
  328. package/src/react/persistedState.ts +0 -40
  329. package/src/react/registry.ts +0 -48
  330. package/src/react/right-panel-registry.tsx +0 -47
  331. package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
  332. package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
  333. package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
  334. package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
  335. package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
  336. package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
  337. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
  338. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
  339. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
  340. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
  341. package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
  342. package/src/react/schemaRenderer/action/buttons.tsx +0 -99
  343. package/src/react/schemaRenderer/action/helpers.ts +0 -140
  344. package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
  345. package/src/react/schemaRenderer/columnFormat.ts +0 -65
  346. package/src/react/schemaRenderer/constants.ts +0 -50
  347. package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
  348. package/src/react/schemaRenderer/form/renderField.tsx +0 -511
  349. package/src/react/schemaRenderer/helpers.tsx +0 -81
  350. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
  351. package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
  352. package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
  353. package/src/react/schemaRenderer/table/filters.tsx +0 -1233
  354. package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
  355. package/src/react/schemaRenderer/table/links.tsx +0 -112
  356. package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
  357. package/src/react/schemaRenderer/table/url.tsx +0 -143
  358. package/src/react/theme-preview/apply.ts +0 -99
  359. package/src/react/theme-preview/build-html.ts +0 -436
  360. package/src/react/ui/button.tsx +0 -51
  361. package/src/react/ui/calendar.tsx +0 -67
  362. package/src/react/ui/checkbox.tsx +0 -29
  363. package/src/react/ui/dialog.tsx +0 -108
  364. package/src/react/ui/dropdown-menu.tsx +0 -97
  365. package/src/react/ui/input.tsx +0 -20
  366. package/src/react/ui/label.tsx +0 -21
  367. package/src/react/ui/popover.tsx +0 -50
  368. package/src/react/ui/select.tsx +0 -169
  369. package/src/react/ui/separator.tsx +0 -25
  370. package/src/react/ui/sheet.tsx +0 -136
  371. package/src/react/ui/sidebar.tsx +0 -723
  372. package/src/react/ui/skeleton.tsx +0 -13
  373. package/src/react/ui/slider.tsx +0 -34
  374. package/src/react/ui/switch.tsx +0 -28
  375. package/src/react/ui/table.tsx +0 -105
  376. package/src/react/ui/tabs.tsx +0 -63
  377. package/src/react/ui/textarea.tsx +0 -18
  378. package/src/react/ui/tooltip.tsx +0 -64
  379. package/src/react/useCollabSeed.ts +0 -86
  380. package/src/react/useResizableWidth.ts +0 -139
  381. package/src/react/utils.ts +0 -6
  382. package/src/react/widgetRegistry.test.ts +0 -43
  383. package/src/react/widgetRegistry.ts +0 -50
  384. package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
  385. package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
  386. package/src/react/widgets/ViewRenderer.tsx +0 -71
  387. package/src/relationManagerData.test.ts +0 -1595
  388. package/src/richtext/index.ts +0 -8
  389. package/src/richtext/registry.ts +0 -89
  390. package/src/routes/globals.ts +0 -148
  391. package/src/routes/guard.test.ts +0 -325
  392. package/src/routes/helpers.ts +0 -700
  393. package/src/routes/pages.ts +0 -175
  394. package/src/routes/panel.ts +0 -204
  395. package/src/routes/relations.ts +0 -1227
  396. package/src/routes/resources.ts +0 -781
  397. package/src/routes/theme.ts +0 -91
  398. package/src/routes-nested-relations.test.ts +0 -676
  399. package/src/routes-relations.test.ts +0 -972
  400. package/src/routes.test.ts +0 -2027
  401. package/src/routes.ts +0 -303
  402. package/src/schema/Alert.test.ts +0 -109
  403. package/src/schema/Alert.ts +0 -131
  404. package/src/schema/Block.ts +0 -169
  405. package/src/schema/Breadcrumbs.ts +0 -40
  406. package/src/schema/Card.ts +0 -35
  407. package/src/schema/Divider.ts +0 -20
  408. package/src/schema/Element.ts +0 -219
  409. package/src/schema/EmptyState.test.ts +0 -37
  410. package/src/schema/EmptyState.ts +0 -63
  411. package/src/schema/Fieldset.ts +0 -43
  412. package/src/schema/Grid.ts +0 -43
  413. package/src/schema/Group.ts +0 -30
  414. package/src/schema/Heading.ts +0 -39
  415. package/src/schema/Html.ts +0 -67
  416. package/src/schema/Icon.ts +0 -54
  417. package/src/schema/Image.ts +0 -57
  418. package/src/schema/LinkTag.ts +0 -41
  419. package/src/schema/Markdown.ts +0 -85
  420. package/src/schema/MetaTag.ts +0 -41
  421. package/src/schema/RelationTabs.ts +0 -71
  422. package/src/schema/ScriptTag.ts +0 -55
  423. package/src/schema/Section.ts +0 -160
  424. package/src/schema/ServerDataElement.test.ts +0 -140
  425. package/src/schema/ServerDataElement.ts +0 -156
  426. package/src/schema/SlotComponent.test.ts +0 -77
  427. package/src/schema/SlotComponent.ts +0 -71
  428. package/src/schema/Split.ts +0 -50
  429. package/src/schema/Stat.test.ts +0 -118
  430. package/src/schema/Stat.ts +0 -154
  431. package/src/schema/StatsOverview.test.ts +0 -141
  432. package/src/schema/StatsOverview.ts +0 -119
  433. package/src/schema/StyleTag.ts +0 -35
  434. package/src/schema/TableWidget.test.ts +0 -297
  435. package/src/schema/TableWidget.ts +0 -289
  436. package/src/schema/Tabs.ts +0 -79
  437. package/src/schema/Text.ts +0 -58
  438. package/src/schema/UnorderedList.ts +0 -49
  439. package/src/schema/View.test.ts +0 -111
  440. package/src/schema/View.ts +0 -127
  441. package/src/schema/Wizard.ts +0 -220
  442. package/src/schema/containers.test.ts +0 -564
  443. package/src/schema/headTags.test.ts +0 -134
  444. package/src/schema/index.ts +0 -40
  445. package/src/schema/primes.test.ts +0 -269
  446. package/src/schema/resolveSchema.test.ts +0 -379
  447. package/src/schema/resolveSchema.ts +0 -917
  448. package/src/schema/sanitize.ts +0 -58
  449. package/src/search.test.ts +0 -446
  450. package/src/search.ts +0 -178
  451. package/src/sessionFilters.test.ts +0 -375
  452. package/src/sessionFilters.ts +0 -143
  453. package/src/slot-components/index.ts +0 -10
  454. package/src/slot-components/registry.ts +0 -56
  455. package/src/styles/file-upload.css +0 -13
  456. package/src/summarizers/Summarizer.test.ts +0 -84
  457. package/src/summarizers/Summarizer.ts +0 -123
  458. package/src/summarizers/index.ts +0 -11
  459. package/src/theme/base-colors.ts +0 -68
  460. package/src/theme/chart-colors.ts +0 -50
  461. package/src/theme/colors.ts +0 -447
  462. package/src/theme/generate-css.test.ts +0 -139
  463. package/src/theme/generate-css.ts +0 -44
  464. package/src/theme/generate-scale.test.ts +0 -106
  465. package/src/theme/generate-scale.ts +0 -97
  466. package/src/theme/icon-map.ts +0 -42
  467. package/src/theme/index.ts +0 -34
  468. package/src/theme/migrate.test.ts +0 -178
  469. package/src/theme/migrate.ts +0 -81
  470. package/src/theme/presets.ts +0 -135
  471. package/src/theme/radius.ts +0 -18
  472. package/src/theme/resolve.test.ts +0 -238
  473. package/src/theme/resolve.ts +0 -96
  474. package/src/theme/spacing.ts +0 -18
  475. package/src/theme/storage.test.ts +0 -126
  476. package/src/theme/storage.ts +0 -106
  477. package/src/theme/theme-colors.ts +0 -88
  478. package/src/theme/types.ts +0 -125
  479. package/src/uploads/UploadAdapter.ts +0 -35
  480. package/src/uploads/index.ts +0 -2
  481. package/src/uploads/localUpload.test.ts +0 -70
  482. package/src/uploads/localUpload.ts +0 -84
  483. package/src/validation/Validator.ts +0 -49
  484. package/src/validation/index.ts +0 -28
  485. package/src/validation/rules.ts +0 -78
  486. package/src/validation/runValidators.ts +0 -435
  487. package/src/validation/uniqueValidator.test.ts +0 -196
  488. package/src/validation/uniqueValidator.ts +0 -133
  489. package/src/validation/validators.test.ts +0 -268
  490. package/src/vite.test.ts +0 -184
  491. package/src/vite.ts +0 -787
  492. package/src/widgets/index.ts +0 -10
  493. package/src/widgets/registry.ts +0 -45
  494. package/src/widgets.test.ts +0 -592
  495. package/tsconfig.build.json +0 -11
  496. package/tsconfig.json +0 -4
  497. package/tsconfig.test.json +0 -10
  498. package/views/react/Dashboard.tsx +0 -27
  499. package/views/react/Resources/Form.tsx +0 -102
  500. package/views/react/Resources/Index.tsx +0 -49
@@ -0,0 +1,198 @@
1
+ # Validation
2
+
3
+ Validators are functions: `(value, ctx?) => string | null | Promise<string | null>`. Return a message string to fail; return `null` to pass. The framework runs them in declaration order, awaits async ones, and aggregates per-field errors before calling the form's `save` handler.
4
+
5
+ ## Built-in validators
6
+
7
+ ```ts
8
+ import { Field } from '@pilotiq/pilotiq'
9
+
10
+ TextField.make('email')
11
+ .validate([
12
+ Field.required(), // or just .required()
13
+ Field.email(),
14
+ Field.maxLength(255),
15
+ ])
16
+
17
+ TextField.make('password')
18
+ .password()
19
+ .validate([
20
+ Field.required(),
21
+ Field.minLength(8, 'At least 8 characters'),
22
+ Field.pattern(/[A-Z]/, 'Must contain an uppercase letter'),
23
+ Field.pattern(/[0-9]/, 'Must contain a digit'),
24
+ ])
25
+
26
+ NumberField.make('age')
27
+ .validate([
28
+ Field.min(18, 'Must be 18 or older'),
29
+ Field.max(120),
30
+ ])
31
+
32
+ TextField.make('slug')
33
+ .validate([
34
+ Field.required(),
35
+ Field.pattern(/^[a-z0-9-]+$/, 'Lowercase letters, numbers, hyphens only'),
36
+ ])
37
+ ```
38
+
39
+ The full set: `required(message?) / email(message?) / minLength(n, message?) / maxLength(n, message?) / min(n, message?) / max(n, message?) / pattern(regex, message)`.
40
+
41
+ `Field.required()` is auto-contributed when you call `.required()` directly — no need to add it twice.
42
+
43
+ ## Custom validators
44
+
45
+ A validator is just a function. Inline:
46
+
47
+ ```ts
48
+ TextField.make('username')
49
+ .validate(async (value, ctx) => {
50
+ if (typeof value !== 'string') return null
51
+ if (value.length < 3) return 'At least 3 characters'
52
+ if (value.startsWith('admin')) return 'Reserved prefix'
53
+ return null // pass
54
+ })
55
+ ```
56
+
57
+ The `ctx` shape:
58
+
59
+ ```ts
60
+ {
61
+ record?: unknown // current record on edit/view
62
+ values?: Record<string, unknown> // all form values
63
+ user?: unknown
64
+ mode?: 'create' | 'edit' | 'view'
65
+ basePath?: string
66
+ }
67
+ ```
68
+
69
+ Async is fine — the framework awaits each one.
70
+
71
+ ## `Field.unique()` async DB probe
72
+
73
+ The standard "this field must be unique across all records" check:
74
+
75
+ ```ts
76
+ import { Field } from '@pilotiq/pilotiq'
77
+
78
+ TextField.make('slug')
79
+ .validate(Field.unique({
80
+ model: Article,
81
+ column: 'slug', // optional, defaults to field name
82
+ ignoreRecord: true, // skip the row matching ctx.record[pk]
83
+ where: { status: 'published' }, // optional scope
84
+ caseInsensitive: true,
85
+ message: 'Slug is already in use', // optional custom message
86
+ }))
87
+ ```
88
+
89
+ How it works:
90
+
91
+ - Issues `M.query().where(column, value).paginate(1, 2)` — limit 2 to detect uniqueness without scanning the table.
92
+ - `ignoreRecord: true` (default `true`) skips the row matching `ctx.record[primaryKey]` so edit-no-change saves don't conflict.
93
+ - `caseInsensitive: true` switches to SQL `LIKE` with `%` / `_` / `\` escaped (SQLite + MySQL friendly; Postgres collation-dependent).
94
+ - `where: { status: 'published' }` adds AND-clauses to the lookup query (useful for soft scopes — "unique among published rows").
95
+ - Inside a `Repeater`, `unique()` probes the database but does NOT see unsaved sibling rows. Pair with `distinct()` for cross-row uniqueness within the form.
96
+
97
+ `Field.unique` accepts a `Model`-like object (anything with `.query()`). The Resource doesn't need to be using the same Model.
98
+
99
+ ## `Field.distinct()` cross-row uniqueness inside Repeater / Builder
100
+
101
+ Inside a `Repeater` or `Builder`, `unique()` only checks the database. To enforce that values are unique ACROSS rows in the form itself (before submit), use `distinct()`:
102
+
103
+ ```ts
104
+ Repeater.make('contacts')
105
+ .schema([
106
+ TextField.make('email')
107
+ .validate(Field.email())
108
+ .distinct(), // unique within this Repeater
109
+ TextField.make('label'),
110
+ ])
111
+ ```
112
+
113
+ Options:
114
+
115
+ ```ts
116
+ TextField.make('email')
117
+ .distinct({
118
+ caseInsensitive: true, // default false
119
+ ignoreNulls: true, // default true — skip empty rows
120
+ message: 'Email already used in another row',
121
+ })
122
+ ```
123
+
124
+ For `Builder`, distinctness is per-block-type — `heading.text="X"` never conflicts with `paragraph.text="X"`.
125
+
126
+ Pair with `unique({ model })` for in-form + cross-record uniqueness:
127
+
128
+ ```ts
129
+ TextField.make('slug')
130
+ .validate(Field.unique({ model: Article })) // unique in DB
131
+ .distinct() // unique in this form too
132
+ ```
133
+
134
+ ## Form-level validators
135
+
136
+ `Form.validate(fn)` runs after every field's validators have passed:
137
+
138
+ ```ts
139
+ form
140
+ .schema([
141
+ DateField.make('startsAt').required(),
142
+ DateField.make('endsAt').required(),
143
+ ])
144
+ .validate(({ values }) => {
145
+ if (values.endsAt < values.startsAt) {
146
+ return { endsAt: 'Must be after start date' }
147
+ }
148
+ return null
149
+ })
150
+ ```
151
+
152
+ Return shape:
153
+ - `null` (or `{}`) — pass
154
+ - `{ [fieldName]: 'message' }` — per-field errors (replaces any field-level errors)
155
+ - `{ _form: 'Top-level message' }` — form-wide error, rendered at the top of the form
156
+
157
+ Form-level validators run AFTER coercion (so values are typed) but BEFORE save.
158
+
159
+ ## Validation order
160
+
161
+ For each form submit, the framework runs:
162
+
163
+ 1. **Coerce raw FormData values** to typed values (string → number for NumberField, JSON-string → object for KeyValueField, etc.). Coerce errors aren't reported per-field; raw values fall through.
164
+ 2. **Field-level validators** — for every Field with `validate([...])` or `.required()`. Aggregated by field name.
165
+ 3. **Form-level `validate(fn)`** — if present, runs only when no field errors fired (early exit).
166
+ 4. **`save(ctx)` / model.create / model.update** — only reached when validation passes.
167
+
168
+ Important: **validate runs BEFORE coerce in `dispatchFormSubmit`**, then field-types do their own coerce-fold during their `runValidators`. The order is "validate the raw value, then coerce the validated value" — useful to remember when writing custom validators.
169
+
170
+ ## Errors on the wire
171
+
172
+ Field errors land on `FormMeta.errors` as `Record<fieldName, string[]>`. The renderer auto-stamps them under each field's input. Repeater/Builder errors key as `items.<i>.<name>` / `name.<i>.data.<child>` respectively; `min/maxItems` lands under the bare field name.
173
+
174
+ For form-level errors, the `_form` key is special — the renderer surfaces it as an Alert at the top of the form.
175
+
176
+ ## Throwing validators
177
+
178
+ A validator that throws is treated as a failure. The message comes from `err.message`:
179
+
180
+ ```ts
181
+ TextField.make('email')
182
+ .validate(async (value) => {
183
+ if (typeof value !== 'string') throw new Error('Email is required')
184
+ if (!value.includes('@')) return 'Invalid email'
185
+ return null
186
+ })
187
+ ```
188
+
189
+ For unique-across-DB or other async lookups, prefer returning `null` for "skip the check" rather than throwing — throws log + fail with the error message verbatim.
190
+
191
+ ## Common pitfalls
192
+
193
+ - **`required()` doesn't see `null` as empty** — by default `required()` checks for `value !== null && value !== undefined && value !== ''`. For domain-specific empty (`[]`, `{}`), write a custom validator.
194
+ - **`Field.unique()` without `ignoreRecord: true`** trips on edit-no-change saves — the row's own value conflicts with itself. Default is `true`; only set to `false` for "even my own row counts."
195
+ - **`distinct()` outside a Repeater/Builder** is a no-op. The framework only evaluates it inside an array-row container.
196
+ - **Async validators in parallel** — validators within ONE field run serially in declaration order; validators across DIFFERENT fields run in parallel. Don't rely on cross-field ordering.
197
+ - **`pattern(/regex/)` without anchors** matches anywhere in the string. Add `^…$` if you mean "the whole value."
198
+ - **Validation result `{}`** (empty object) means "pass" — the framework treats no-keys as no-errors. Return `null` for clarity.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: pilotiq-relations
3
+ description: Wiring related entities in pilotiq — RelationManager tabs (hasMany / morph / belongsToMany), Repeater.relationship for inline child rows, and Builder.relationship for heterogeneous child types
4
+ license: MIT
5
+ appliesTo:
6
+ - '@pilotiq/pilotiq'
7
+ trigger: defining `Resource.relations()`, subclassing `RelationManager`, or wiring `Repeater.relationship` / `Builder.relationship` for relation-backed array rows
8
+ skip: defining standalone fields with no parent-child semantics — that's `pilotiq-fields`
9
+ metadata:
10
+ author: pilotiq
11
+ ---
12
+
13
+ # Pilotiq Relations
14
+
15
+ ## When to use this skill
16
+
17
+ Load when you're:
18
+
19
+ - Adding a `comments` / `tags` / `attachments` tab to a record's edit page via a `RelationManager`
20
+ - Choosing between a `RelationManager` (separate tab, full table + form) and `Repeater.relationship()` (inline rows on the parent form)
21
+ - Wiring a polymorphic (`morphMany` / `morphTo`) or many-to-many (`belongsToMany`) relation
22
+ - Building a Builder field whose rows persist as real child records with `{type, data}` shape (`Builder.relationship`)
23
+
24
+ For standalone form fields (no relation), use `pilotiq-fields`. For Resource basics, `pilotiq-resource`.
25
+
26
+ ## Quick Reference
27
+
28
+ | Task | Open |
29
+ |---|---|
30
+ | RelationManager — separate tab on the parent's edit/view page; full table + form for the related records. Covers hasMany / morph / M2M | `rules/relation-managers.md` |
31
+ | Repeater.relationship — inline rows on the parent form backed by real `hasMany` / `morph*` / M2M children. Builder.relationship for `{type,data}` heterogeneous rows | `rules/repeater-relationship.md` |
32
+
33
+ ## Key concepts (load once)
34
+
35
+ - **Two patterns, different UX.** A `RelationManager` is a separate tab with its own table — good for many children (Posts → Comments). A `Repeater.relationship()` is inline rows on the parent form — good for tight 1-to-few (Order → LineItems).
36
+ - **`RelationManager` requires `static relationName` to match a key on the parent model's `static relations` map.** That string doubles as URL segment (`/posts/:id/comments`) and the relation accessor (`parent.related('comments')`).
37
+ - **`RelationManager.mode` is auto-derived.** From `parent.constructor.relations[relationName].type` via `getRelationType + normalizeRelationMode`. `hasOne` / `hasMany` → `'hasMany'`; `morphMany` / `morphOne` → `'morphMany'`; `morphTo` → `'morphTo'`; M2M (`belongsToMany`, `morphToMany`, `morphedByMany`) → `'belongsToMany'`. Forms + actions adapt accordingly — M2M flips into pivot-mutation mode; morphMany auto-fills `<morphName>Id` / `<morphName>Type` on create + edit.
38
+ - **`Repeater.relationship` persists rows as real children.** Diffs submitted rows vs `parent.related(rel).get()` on save — matching `__id` runs `M.update`, missing ID runs `M.create`, existing PK absent from submitted set runs `M.delete`. M2M variant calls `accessor.attach/detach` instead of `M.delete`.
39
+ - **`Builder.relationship` adds a discriminator column** (default `'type'`, the block name) + a JSON payload column (default `'data'`, the per-block inner-schema values). Same diff persistence as Repeater.relationship, but each row carries its block type so the form can render the right inner schema.
40
+ - **Authorization is two-layered.** Parent's `canView` / `canEdit` runs first; then the manager's `canViewAny` / `canCreate` / `canEdit` / `canDelete` (or `canAttach` / `canDetach` for M2M). Fall-through: manager predicates default to the related Resource's matching predicate when the manager hasn't overridden (except `canAttach` / `canDetach` — manager-only, no fall-through).
41
+
42
+ ## Examples
43
+
44
+ - `playground/app/Pilotiq/Posts/RelationManagers/CommentsRelationManager.ts` — vanilla `hasMany` manager.
45
+ - `playground/app/Pilotiq/Articles/RelationManagers/TagsRelationManager.ts` — `belongsToMany` with attach/detach.
46
+ - `playground/app/Pilotiq/Comments/CommentResource.ts` — `morphTo` (child-side polymorphic) shared across multiple parents.
47
+ - `playground/app/Pilotiq/Orders/Schemas/form.ts` — `Repeater.relationship('lineItems')` for inline child rows.
@@ -0,0 +1,256 @@
1
+ # Relation Managers
2
+
3
+ A `RelationManager` projects a related collection onto a parent record's edit / view page as a separate tab — full table chrome, create / edit / delete actions, optional attach / detach for M2M. Each manager runs against `parent.related(relationName).query()` and uses the related Resource's `Model` for persistence.
4
+
5
+ ## Basics — `hasMany`
6
+
7
+ ```ts
8
+ import { RelationManager, Table, Form, Column, TextField, Textarea } from '@pilotiq/pilotiq'
9
+
10
+ export class CommentsRelationManager extends RelationManager {
11
+ static override relationName = 'comments' // matches Post.relations.comments
12
+ static override label = 'Comments'
13
+ static override labelSingular = 'Comment'
14
+ static override icon = 'message-square'
15
+ static override recordTitleAttribute = 'body'
16
+
17
+ static override form(form: Form, ctx) {
18
+ return form.schema([
19
+ Textarea.make('body').required().rows(4),
20
+ ])
21
+ }
22
+
23
+ static override table(table: Table, ctx) {
24
+ return table
25
+ .columns([
26
+ Column.make('body').limit(80).searchable(),
27
+ Column.make('author.name').label('Author'),
28
+ Column.make('createdAt').since().sortable(),
29
+ ])
30
+ .defaultSort('createdAt', 'desc')
31
+ .paginate(10)
32
+ .recordActions([
33
+ Action.relationEdit(this, ctx),
34
+ Action.relationDelete(this, ctx),
35
+ ])
36
+ .headerActions([
37
+ Action.relationCreate(this, ctx),
38
+ ])
39
+ }
40
+ }
41
+
42
+ class PostResource extends Resource {
43
+ static override model = Post
44
+ static override relations() {
45
+ return [CommentsRelationManager]
46
+ }
47
+ }
48
+ ```
49
+
50
+ The parent (`Post`) must declare the relation in its `static relations`:
51
+
52
+ ```ts
53
+ export class Post extends Model {
54
+ static override relations = {
55
+ comments: { type: 'hasMany', model: () => Comment, foreignKey: 'postId' },
56
+ }
57
+ }
58
+ ```
59
+
60
+ That gives you:
61
+
62
+ - New tab on `/admin/posts/:id` and `/admin/posts/:id/edit` labeled "Comments"
63
+ - Tab content is the manager's `table()` — rows from `Post.find(id).related('comments').query()`
64
+ - Routes: `GET ${base}/posts/:id/comments` (list), `GET/POST .../comments/create`, `GET/POST .../comments/:childId/edit`, `POST .../comments/:childId/delete`
65
+ - IDOR check on edit/delete: framework re-runs the relation query before each, throws 404 if the child no longer belongs to the parent
66
+
67
+ `ctx: RelationManagerContext` carries `basePath / parentSlug / parentId / relationship / parentRecord / mode` so factories can wire URLs without manual threading.
68
+
69
+ ## Polymorphic — `morphMany` / `morphTo`
70
+
71
+ Parent-side (`morphMany`):
72
+
73
+ ```ts
74
+ // Post.ts
75
+ export class Post extends Model {
76
+ static override relations = {
77
+ comments: { type: 'morphMany', model: () => Comment, name: 'commentable' },
78
+ }
79
+ }
80
+
81
+ // Video.ts
82
+ export class Video extends Model {
83
+ static override relations = {
84
+ comments: { type: 'morphMany', model: () => Comment, name: 'commentable' },
85
+ }
86
+ }
87
+
88
+ // Comment.ts (child-side morphTo)
89
+ export class Comment extends Model {
90
+ static override relations = {
91
+ commentable: { type: 'morphTo', name: 'commentable' },
92
+ }
93
+ commentableId!: string
94
+ commentableType!: 'Post' | 'Video'
95
+ body!: string
96
+ }
97
+ ```
98
+
99
+ Same manager for both parents — register it on `PostResource.relations()` and `VideoResource.relations()`. The framework auto-fills `commentableId = parent.id` and `commentableType = 'Post' | 'Video'` (read from `parent.constructor.morphAlias ?? parent.constructor.name`) on create + edit. **The framework wins last** — a tampered POST body (`commentableId=v1&commentableType=Video`) cannot reassign a child to a different polymorphic parent.
100
+
101
+ Child-side (`morphTo`) — the child class itself can be a Resource, but doesn't get auto-actions or auto-discovery:
102
+
103
+ ```ts
104
+ export class CommentResource extends Resource {
105
+ static override model = Comment
106
+ // The morphTo column drives display only; the parent is dynamic
107
+ }
108
+ ```
109
+
110
+ Set `static relatedResource = SomeResource` explicitly on the manager if you want a custom view of the comment.
111
+
112
+ ## Many-to-many — `belongsToMany`
113
+
114
+ ```ts
115
+ // Article.ts
116
+ export class Article extends Model {
117
+ static override relations = {
118
+ tags: { type: 'belongsToMany', model: () => Tag, pivot: 'article_tag' },
119
+ }
120
+ }
121
+
122
+ // Tag.ts
123
+ export class Tag extends Model {
124
+ static override relations = {
125
+ articles: { type: 'belongsToMany', model: () => Article, pivot: 'article_tag' },
126
+ }
127
+ }
128
+
129
+ // TagsRelationManager.ts
130
+ export class TagsRelationManager extends RelationManager {
131
+ static override relationName = 'tags'
132
+ static override label = 'Tags'
133
+
134
+ static override table(table: Table, ctx) {
135
+ return table
136
+ .columns([
137
+ Column.make('name').searchable(),
138
+ Column.make('slug'),
139
+ ])
140
+ .headerActions([
141
+ Action.relationAttach(this, ctx), // modal picker
142
+ ])
143
+ .recordActions([
144
+ Action.relationDetach(this, ctx), // unlink (don't delete tag)
145
+ ])
146
+ .bulkActions([
147
+ Action.relationBulkDetach(this, ctx),
148
+ ])
149
+ }
150
+
151
+ // M2M-only authorization predicates
152
+ static override async canAttach(user, parentRecord) { return Boolean(user) }
153
+ static override async canDetach(user, child, parentRecord) { return user.role === 'admin' }
154
+ }
155
+ ```
156
+
157
+ The framework dispatches via `parent[relationName]().attach() / .detach()` instead of `M.create() / M.delete()`. Important distinctions:
158
+
159
+ - **`relationDetach` unlinks the pivot row only** — the related `Tag` still exists. `relationDelete` (which would delete the Tag itself) auto-hides under M2M.
160
+ - **`relationAttach` modal-form** uses a `SelectField` populated by `loadAttachableCandidates()` — fetches up to 50 candidate rows server-side and filters out already-attached IDs.
161
+ - **`relationCreate` / `relationEdit`** still auto-hide under M2M — the existing tag is edited via its own `TagResource` route, not the relation manager.
162
+
163
+ Pivot extras (columns on the `article_tag` pivot itself) aren't editable through `RelationManager` in v1 — see `Repeater.relationship().pivotColumns([…])` for that pattern, or use a `Repeater.relationship` instead.
164
+
165
+ ## Authorization — manager + Resource fall-through
166
+
167
+ `RelationManager` exposes seven async predicates:
168
+
169
+ ```ts
170
+ class CommentsRelationManager extends RelationManager {
171
+ static override async canViewAny(user, parentRecord) { return true }
172
+ static override async canView(user, child, parentRecord) { return true }
173
+ static override async canCreate(user, parentRecord) { return Boolean(user) }
174
+ static override async canEdit(user, child, parentRecord) { return user.id === child.authorId }
175
+ static override async canDelete(user, child, parentRecord) { return user.role === 'admin' }
176
+ static override async canAttach(user, parentRecord) { return false } // not M2M
177
+ static override async canDetach(user, child, parentRecord) { return false }
178
+ }
179
+ ```
180
+
181
+ Fall-through behavior:
182
+
183
+ - Predicates that ARE overridden on the manager: use the manager's value.
184
+ - Predicates that are NOT overridden: fall through to the related Resource's matching predicate via reference-equality check on the prototype.
185
+ - `canAttach` / `canDetach` are manager-only — they DON'T fall through (attach/detach are pivot operations, not record operations).
186
+
187
+ For the route handler, the framework runs `parent.canAccess + parent.canEdit` first, then the manager-scope predicate. Both must pass.
188
+
189
+ ## Reserved relation tokens
190
+
191
+ Relation names are validated at panel boot. The following tokens are reserved and throw a clear error if used as `relationName`:
192
+
193
+ `edit`, `delete`, `restore`, `force-delete`, `_form`, `_action`, `_search`, `_uploads`, `_attach`, `_detach`, `_bulk-detach`
194
+
195
+ If you have a relation that collides (rare), rename the relation on the Model.
196
+
197
+ ## Soft-delete on relation children
198
+
199
+ Same two-sided opt-in as Resources. When the related Model AND the related Resource both declare `softDeletes = true`, the manager auto-injects `TrashedFilter`, and `Action.relationRestore` / `relationForceDelete` factories become available.
200
+
201
+ ```ts
202
+ class CommentsRelationManager extends RelationManager {
203
+ static override relationName = 'comments'
204
+
205
+ static override table(table, ctx) {
206
+ return table
207
+ .columns([...])
208
+ .recordActions([
209
+ Action.relationEdit(this, ctx),
210
+ Action.relationDelete(this, ctx), // shows on active rows
211
+ Action.relationRestore(this, ctx), // shows on trashed rows
212
+ Action.relationForceDelete(this, ctx), // shows on trashed rows
213
+ ])
214
+ }
215
+ }
216
+ ```
217
+
218
+ ## Replicate (clone) a child
219
+
220
+ ```ts
221
+ .recordActions([
222
+ Action.relationReplicate(this, ctx, undefined, {
223
+ excludeAttributes: ['publishedAt'], // strip these from the clone
224
+ beforeReplicaSaved: (replica, ctx) => {
225
+ replica.body = `[Copy] ${replica.body}`
226
+ return replica
227
+ },
228
+ }),
229
+ ])
230
+ ```
231
+
232
+ The framework strips PK + soft-delete column + your `excludeAttributes`, runs `beforeReplicaSaved`, then **force-pins the parent attachment column back** so a tampered source row can't slip a different parent in by riding its own FK column. Auto-hides on M2M (replicate doesn't fit pivot semantics) and on `morphTo` (no single owner to pin to).
233
+
234
+ ## Nested relations (depth-2)
235
+
236
+ A manager can register its own sub-managers — a Post → Comments → CommentReplies chain:
237
+
238
+ ```ts
239
+ class CommentsRelationManager extends RelationManager {
240
+ static override relations() {
241
+ return [CommentRepliesRelationManager]
242
+ }
243
+ }
244
+ ```
245
+
246
+ The Comments tab on a Post shows the regular comments table; clicking a comment opens its edit/view page with the CommentReplies sub-tab. Sub-manager URLs are `${base}/posts/:postId/comments/:commentId/replies`.
247
+
248
+ Depth-2 supports the full surface of depth-1 (form, table, actions, soft delete, replicate, M2M, polymorphic). Depth-3+ deferred — you'd usually denormalize at that point.
249
+
250
+ ## Common pitfalls
251
+
252
+ - **`relationName` typo** — silently makes the manager point at a non-existent relation. The framework catches it at boot if the parent's `static relations` map doesn't contain the key (clear error message). If you skip declaring relations on the parent Model, the M2M / morph type can't be detected and falls back to `'hasMany'` — also caught at boot with a clear warning.
253
+ - **`Action.relationEdit / relationDelete` outside `RelationManager.table()`** doesn't work — they need the `ctx` arg from the manager. Use `Action.edit(R, base, id)` for the related Resource's standalone edit page.
254
+ - **Forgetting `static relatedResource`** on a `morphTo` manager means the framework can't resolve form / detail schemas for the child. Set it explicitly when the child is polymorphic.
255
+ - **M2M `canCreate` semantics** — for M2M, `canCreate` controls whether the user can create a NEW tag (via the regular TagResource path). Use `canAttach` to control whether they can link an existing tag to this parent.
256
+ - **Pivot reads aren't surfaced via `belongsToMany` v1** — if you need extra columns on the pivot table (e.g. `created_at` on `article_tag`), use a `Repeater.relationship('articleTags')` with the pivot model as a regular `hasMany` instead. The `RelationManager` route layer can't expose pivot extras without ORM changes.
@@ -0,0 +1,177 @@
1
+ # Repeater.relationship and Builder.relationship
2
+
3
+ `Repeater.relationship(name)` and `Builder.relationship(name)` are the inline-row alternatives to `RelationManager`. Instead of a separate tab with its own table, rows live ON the parent's form — typed inline, persisted to a real `hasMany` / `morph*` / M2M relation (Repeater) or a discriminator + JSON-payload child table (Builder).
4
+
5
+ Use these for tight 1-to-few relations where you want inline editing on the parent form: line items on an order, slides in a presentation, blocks in a CMS page.
6
+
7
+ ## `Repeater.relationship` — uniform rows
8
+
9
+ ```ts
10
+ Repeater.make('lineItems')
11
+ .relationship('lineItems')
12
+ .schema([
13
+ TextField.make('description').required(),
14
+ NumberField.make('quantity').min(1).required(),
15
+ NumberField.make('unitPrice').step(0.01).required(),
16
+ ])
17
+ .min(1)
18
+ .reorderable()
19
+ .orderColumn('position') // optional — stamp index on save
20
+ ```
21
+
22
+ The parent Model must declare the relation:
23
+
24
+ ```ts
25
+ export class Order extends Model {
26
+ static override relations = {
27
+ lineItems: { type: 'hasMany', model: () => LineItem, foreignKey: 'orderId' },
28
+ }
29
+ }
30
+
31
+ export class LineItem extends Model {
32
+ static override table = 'line_items'
33
+ description!: string
34
+ quantity!: number
35
+ unitPrice!: number
36
+ position?: number // when orderColumn is set
37
+ }
38
+ ```
39
+
40
+ How it works:
41
+
42
+ - **Load** — `applyRelationshipRepeaterFill()` reads rows from `parent.related('lineItems')` via the relation accessor, stamps `__id = String(child.pk)` on each, strips PK + FK from the rendered row.
43
+ - **Save** — `dispatchFormSubmit` extracts the field's value before generic field coercion, then after the parent's `save()` returns runs `persistRelationshipRows`:
44
+ - Submitted rows with `__id` matching an existing PK → `M.update(__id, row)`. FK is NOT overwritten (defense against tampered re-link).
45
+ - Submitted rows with `__id` absent or non-matching → `M.create({ ...row, [foreignKey]: parentPk })`.
46
+ - Existing PKs missing from the submitted set → `M.delete(pk)`.
47
+ - When `orderColumn` is set: the row's 0-based index stamps on every create + update payload.
48
+
49
+ **M2M variant** — when the relation is `belongsToMany` / `morphToMany` / `morphedByMany`, the framework dispatches through `parent[rel]().attach()` / `.detach()` instead. Row-create calls `M.create()` then `accessor.attach([newPk])`. Row-remove calls `accessor.detach([pk])` only — no `M.delete` (the related child may be linked to other parents).
50
+
51
+ ```ts
52
+ // On Article: tags via M2M
53
+ Repeater.make('tags')
54
+ .relationship('tags') // Article.tags = belongsToMany
55
+ .schema([
56
+ SelectField.make('id').options(allTagsAsOptions).required(),
57
+ ])
58
+ ```
59
+
60
+ `pivotColumns([…])` adds editable columns on the pivot:
61
+
62
+ ```ts
63
+ Repeater.make('tags')
64
+ .relationship('tags')
65
+ .schema([
66
+ SelectField.make('id').options(allTagsAsOptions).required(),
67
+ ])
68
+ .pivotColumns([
69
+ NumberField.make('weight').default(1), // editable column on article_tag
70
+ TextField.make('note'),
71
+ ])
72
+ ```
73
+
74
+ Pivot columns are read from the M2M pivot row, edited inline, persisted via `accessor.sync()` or `attach/detach`-with-pivot APIs.
75
+
76
+ ## `Builder.relationship` — heterogeneous rows
77
+
78
+ When rows can be ONE OF N block types — paragraph vs heading vs image — `Builder.relationship` persists each row as a child record with a discriminator column + a JSON payload column:
79
+
80
+ ```ts
81
+ Builder.make('content')
82
+ .relationship('blocks') // parent.blocks = hasMany ContentBlock
83
+ .blocks([
84
+ Block.make('heading').icon('heading').schema([
85
+ TextField.make('text').required(),
86
+ SelectField.make('level').options({ h1: 'H1', h2: 'H2', h3: 'H3' }),
87
+ ]),
88
+ Block.make('paragraph').icon('text').schema([
89
+ MarkdownField.make('body'),
90
+ ]),
91
+ Block.make('image').icon('image').schema([
92
+ FileUpload.make('src').accept('image/*').required(),
93
+ TextField.make('alt'),
94
+ ]),
95
+ ])
96
+ .reorderable()
97
+ .orderColumn('position')
98
+ ```
99
+
100
+ Schema for `ContentBlock`:
101
+
102
+ ```ts
103
+ export class ContentBlock extends Model {
104
+ static override table = 'content_blocks'
105
+ id!: number
106
+ pageId!: number // foreign key
107
+ type!: string // 'heading' | 'paragraph' | 'image'
108
+ data!: Record<string, unknown> // per-block inner-schema values
109
+ position?: number
110
+ }
111
+ ```
112
+
113
+ Column names are overridable via the options object:
114
+
115
+ ```ts
116
+ Builder.make('content').relationship({
117
+ name: 'blocks',
118
+ typeColumn: 'blockType', // default 'type'
119
+ dataColumn: 'payload', // default 'data'
120
+ orderColumn: 'sortOrder',
121
+ })
122
+ ```
123
+
124
+ How it works:
125
+
126
+ - **Load** — `applyRelationshipBuilderFill()` reads `{__id, type, data}` per row, JSON-parses string `data` columns, strips PK + FK + `type` + `data` from each rendered row's inner data.
127
+ - **Save** — `persistRelationshipBuilderRows()`:
128
+ - Submitted rows with `__id` matching an existing PK → `M.update(__id, { [typeColumn]: row.type, [dataColumn]: row.data, [orderColumn]: idx })`. FK is NOT overwritten.
129
+ - Submitted rows with `__id` absent → `M.create({ [typeColumn]: row.type, [dataColumn]: row.data, [foreignKey]: parentPk, [orderColumn]: idx })`.
130
+ - Existing PKs missing from submitted set → `M.delete(pk)`.
131
+ - **Type column rewrites on update** — a block can switch types between submits.
132
+
133
+ Unknown block types in submitted data round-trip verbatim (renderer shows a placeholder, server passes data through) — config rollbacks never silently lose content.
134
+
135
+ v1 = `hasMany` + `morphMany` / `morphOne` only. M2M is deferred — heterogeneous `{type, data}` envelope doesn't compose cleanly with pivot semantics.
136
+
137
+ ## When to use which
138
+
139
+ | Pattern | Use when |
140
+ |---|---|
141
+ | `RelationManager` (separate tab) | Many children (100s+ comments); separate URL feels natural; user expects pagination + search; permissions differ from parent |
142
+ | `Repeater.relationship` (inline, uniform) | Tight 1-to-few (10ish line items); users edit children alongside the parent; consistent shape per row |
143
+ | `Repeater` (no `.relationship()`, JSON storage) | Same as above but no relation table — rows live as a JSON column on the parent. Simplest setup |
144
+ | `Builder.relationship` (inline, heterogeneous) | CMS content blocks, form-builder schemas — rows have varied shape; need querying child records (`pageId`-indexed) |
145
+ | `Builder` (no `.relationship()`, JSON storage) | Same shape, JSON-blob storage. Use when you don't need to query children individually |
146
+
147
+ ## Per-row hooks
148
+
149
+ ```ts
150
+ Repeater.make('lineItems')
151
+ .relationship('lineItems')
152
+ .schema([...])
153
+ .afterCreate(async (record, ctx) => {
154
+ await audit.log('lineItem.created', { orderId: ctx.parentId, lineItemId: record.id })
155
+ })
156
+ .afterUpdate(async (record, ctx) => {
157
+ // ctx: { parent, parentId, field, index, mode }
158
+ // mode is 'hasMany' | 'morphMany' | 'belongsToMany' | 'morphToMany' | 'morphedByMany'
159
+ })
160
+ .afterDelete(async (record, ctx) => {
161
+ // index is -1 on afterDelete (deleted rows aren't in submitted set)
162
+ })
163
+ ```
164
+
165
+ Each setter throws at config time if `relationship()` wasn't called first.
166
+
167
+ Errors propagate — a throwing handler stops the rest of the persist diff. v1 isn't transactional so earlier rows are already committed.
168
+
169
+ ## Common pitfalls
170
+
171
+ - **`Repeater.relationship()` without a `static relations[name]` entry on the parent Model** throws a clear error pointing at the override paths. Add `{ type: 'hasMany', model: () => Child, foreignKey: 'parentId' }` to the parent's `static relations`.
172
+ - **Mutually exclusive with `simple()` and `dehydrated(false)`** — relationship-backed Repeaters need full row shape. Calling either after `.relationship()` throws.
173
+ - **No transaction wrapper in v1** — partial failure leaves the parent saved with some rows persisted and others not. For critical financial / inventory flows, use a separate `Action.handler` that wraps the save in a Model-level transaction explicitly.
174
+ - **`orderColumn` rejected on M2M** — ORM has no `orderByPivot` in v1; pivot-ordered relations aren't supported. Use a regular `hasMany` with an explicit join table for ordered M2M.
175
+ - **`Builder.relationship` doesn't support M2M** — heterogeneous `{type, data}` envelope doesn't compose with pivot semantics. Use `Repeater.relationship` with M2M, or model as `morphMany` instead.
176
+ - **`pivotColumns` outside an M2M relation** is a no-op — the framework can't write pivot extras on a hasMany. The columns silently don't persist.
177
+ - **Submitting a row with a tampered `__id`** (pointing at another parent's child) trips the framework's IDOR check — `persistRelationshipRows` re-queries via `parent.related(rel)` and refuses to update a child that doesn't belong. The submit returns a 422 with a clear error.