@pilotiq/pilotiq 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1409) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +11 -0
  3. package/CLAUDE.md +207 -0
  4. package/LICENSE +21 -0
  5. package/dist/Cluster.d.ts +56 -0
  6. package/dist/Cluster.d.ts.map +1 -0
  7. package/dist/Cluster.js +62 -0
  8. package/dist/Cluster.js.map +1 -0
  9. package/dist/Column.d.ts +378 -0
  10. package/dist/Column.d.ts.map +1 -0
  11. package/dist/Column.js +434 -0
  12. package/dist/Column.js.map +1 -0
  13. package/dist/Global.d.ts +123 -0
  14. package/dist/Global.d.ts.map +1 -0
  15. package/dist/Global.js +124 -0
  16. package/dist/Global.js.map +1 -0
  17. package/dist/Page.d.ts +90 -0
  18. package/dist/Page.d.ts.map +1 -0
  19. package/dist/Page.js +107 -0
  20. package/dist/Page.js.map +1 -0
  21. package/dist/Pilotiq.d.ts +505 -0
  22. package/dist/Pilotiq.d.ts.map +1 -0
  23. package/dist/Pilotiq.js +463 -0
  24. package/dist/Pilotiq.js.map +1 -0
  25. package/dist/PilotiqRegistry.d.ts +10 -0
  26. package/dist/PilotiqRegistry.d.ts.map +1 -0
  27. package/dist/PilotiqRegistry.js +32 -0
  28. package/dist/PilotiqRegistry.js.map +1 -0
  29. package/dist/PilotiqServiceProvider.d.ts +16 -0
  30. package/dist/PilotiqServiceProvider.d.ts.map +1 -0
  31. package/dist/PilotiqServiceProvider.js +57 -0
  32. package/dist/PilotiqServiceProvider.js.map +1 -0
  33. package/dist/RelationManager.d.ts +372 -0
  34. package/dist/RelationManager.d.ts.map +1 -0
  35. package/dist/RelationManager.js +342 -0
  36. package/dist/RelationManager.js.map +1 -0
  37. package/dist/RenderHook.d.ts +86 -0
  38. package/dist/RenderHook.d.ts.map +1 -0
  39. package/dist/RenderHook.js +116 -0
  40. package/dist/RenderHook.js.map +1 -0
  41. package/dist/Resource.d.ts +290 -0
  42. package/dist/Resource.d.ts.map +1 -0
  43. package/dist/Resource.js +362 -0
  44. package/dist/Resource.js.map +1 -0
  45. package/dist/RightPanel.d.ts +92 -0
  46. package/dist/RightPanel.d.ts.map +1 -0
  47. package/dist/RightPanel.js +61 -0
  48. package/dist/RightPanel.js.map +1 -0
  49. package/dist/Tab.d.ts +92 -0
  50. package/dist/Tab.d.ts.map +1 -0
  51. package/dist/Tab.js +93 -0
  52. package/dist/Tab.js.map +1 -0
  53. package/dist/UserMenuItem.d.ts +76 -0
  54. package/dist/UserMenuItem.d.ts.map +1 -0
  55. package/dist/UserMenuItem.js +87 -0
  56. package/dist/UserMenuItem.js.map +1 -0
  57. package/dist/actions/Action.d.ts +888 -0
  58. package/dist/actions/Action.d.ts.map +1 -0
  59. package/dist/actions/Action.js +1652 -0
  60. package/dist/actions/Action.js.map +1 -0
  61. package/dist/actions/ActionGroup.d.ts +85 -0
  62. package/dist/actions/ActionGroup.d.ts.map +1 -0
  63. package/dist/actions/ActionGroup.js +132 -0
  64. package/dist/actions/ActionGroup.js.map +1 -0
  65. package/dist/actions/attachFactory.d.ts +67 -0
  66. package/dist/actions/attachFactory.d.ts.map +1 -0
  67. package/dist/actions/attachFactory.js +115 -0
  68. package/dist/actions/attachFactory.js.map +1 -0
  69. package/dist/actions/exportFactory.d.ts +88 -0
  70. package/dist/actions/exportFactory.d.ts.map +1 -0
  71. package/dist/actions/exportFactory.js +144 -0
  72. package/dist/actions/exportFactory.js.map +1 -0
  73. package/dist/actions/importFactory.d.ts +97 -0
  74. package/dist/actions/importFactory.d.ts.map +1 -0
  75. package/dist/actions/importFactory.js +143 -0
  76. package/dist/actions/importFactory.js.map +1 -0
  77. package/dist/actions/index.d.ts +3 -0
  78. package/dist/actions/index.d.ts.map +1 -0
  79. package/dist/actions/index.js +3 -0
  80. package/dist/actions/index.js.map +1 -0
  81. package/dist/applyPageHooks.d.ts +54 -0
  82. package/dist/applyPageHooks.d.ts.map +1 -0
  83. package/dist/applyPageHooks.js +149 -0
  84. package/dist/applyPageHooks.js.map +1 -0
  85. package/dist/cells/coerce.d.ts +25 -0
  86. package/dist/cells/coerce.d.ts.map +1 -0
  87. package/dist/cells/coerce.js +87 -0
  88. package/dist/cells/coerce.js.map +1 -0
  89. package/dist/clusterPaths.d.ts +9 -0
  90. package/dist/clusterPaths.d.ts.map +1 -0
  91. package/dist/clusterPaths.js +19 -0
  92. package/dist/clusterPaths.js.map +1 -0
  93. package/dist/columns/BadgeColumn.d.ts +16 -0
  94. package/dist/columns/BadgeColumn.d.ts.map +1 -0
  95. package/dist/columns/BadgeColumn.js +25 -0
  96. package/dist/columns/BadgeColumn.js.map +1 -0
  97. package/dist/columns/BooleanColumn.d.ts +10 -0
  98. package/dist/columns/BooleanColumn.d.ts.map +1 -0
  99. package/dist/columns/BooleanColumn.js +18 -0
  100. package/dist/columns/BooleanColumn.js.map +1 -0
  101. package/dist/columns/ColorColumn.d.ts +27 -0
  102. package/dist/columns/ColorColumn.d.ts.map +1 -0
  103. package/dist/columns/ColorColumn.js +35 -0
  104. package/dist/columns/ColorColumn.js.map +1 -0
  105. package/dist/columns/IconColumn.d.ts +22 -0
  106. package/dist/columns/IconColumn.d.ts.map +1 -0
  107. package/dist/columns/IconColumn.js +28 -0
  108. package/dist/columns/IconColumn.js.map +1 -0
  109. package/dist/columns/ImageColumn.d.ts +17 -0
  110. package/dist/columns/ImageColumn.d.ts.map +1 -0
  111. package/dist/columns/ImageColumn.js +24 -0
  112. package/dist/columns/ImageColumn.js.map +1 -0
  113. package/dist/columns/SelectColumn.d.ts +36 -0
  114. package/dist/columns/SelectColumn.d.ts.map +1 -0
  115. package/dist/columns/SelectColumn.js +52 -0
  116. package/dist/columns/SelectColumn.js.map +1 -0
  117. package/dist/columns/TextColumn.d.ts +18 -0
  118. package/dist/columns/TextColumn.d.ts.map +1 -0
  119. package/dist/columns/TextColumn.js +20 -0
  120. package/dist/columns/TextColumn.js.map +1 -0
  121. package/dist/columns/TextInputColumn.d.ts +47 -0
  122. package/dist/columns/TextInputColumn.d.ts.map +1 -0
  123. package/dist/columns/TextInputColumn.js +60 -0
  124. package/dist/columns/TextInputColumn.js.map +1 -0
  125. package/dist/columns/ToggleColumn.d.ts +32 -0
  126. package/dist/columns/ToggleColumn.d.ts.map +1 -0
  127. package/dist/columns/ToggleColumn.js +45 -0
  128. package/dist/columns/ToggleColumn.js.map +1 -0
  129. package/dist/columns/index.d.ts +10 -0
  130. package/dist/columns/index.d.ts.map +1 -0
  131. package/dist/columns/index.js +10 -0
  132. package/dist/columns/index.js.map +1 -0
  133. package/dist/defaultGlobalPages.d.ts +11 -0
  134. package/dist/defaultGlobalPages.d.ts.map +1 -0
  135. package/dist/defaultGlobalPages.js +87 -0
  136. package/dist/defaultGlobalPages.js.map +1 -0
  137. package/dist/defaultPages.d.ts +247 -0
  138. package/dist/defaultPages.d.ts.map +1 -0
  139. package/dist/defaultPages.js +558 -0
  140. package/dist/defaultPages.js.map +1 -0
  141. package/dist/elements/Form.d.ts +219 -0
  142. package/dist/elements/Form.d.ts.map +1 -0
  143. package/dist/elements/Form.js +259 -0
  144. package/dist/elements/Form.js.map +1 -0
  145. package/dist/elements/ListTabs.d.ts +17 -0
  146. package/dist/elements/ListTabs.d.ts.map +1 -0
  147. package/dist/elements/ListTabs.js +23 -0
  148. package/dist/elements/ListTabs.js.map +1 -0
  149. package/dist/elements/Table.d.ts +535 -0
  150. package/dist/elements/Table.d.ts.map +1 -0
  151. package/dist/elements/Table.js +481 -0
  152. package/dist/elements/Table.js.map +1 -0
  153. package/dist/elements/TableGroup.d.ts +121 -0
  154. package/dist/elements/TableGroup.d.ts.map +1 -0
  155. package/dist/elements/TableGroup.js +162 -0
  156. package/dist/elements/TableGroup.js.map +1 -0
  157. package/dist/elements/dispatchAction.d.ts +127 -0
  158. package/dist/elements/dispatchAction.d.ts.map +1 -0
  159. package/dist/elements/dispatchAction.js +254 -0
  160. package/dist/elements/dispatchAction.js.map +1 -0
  161. package/dist/elements/dispatchForm.d.ts +220 -0
  162. package/dist/elements/dispatchForm.d.ts.map +1 -0
  163. package/dist/elements/dispatchForm.js +1645 -0
  164. package/dist/elements/dispatchForm.js.map +1 -0
  165. package/dist/elements/dispatchTable.d.ts +69 -0
  166. package/dist/elements/dispatchTable.d.ts.map +1 -0
  167. package/dist/elements/dispatchTable.js +606 -0
  168. package/dist/elements/dispatchTable.js.map +1 -0
  169. package/dist/elements/index.d.ts +3 -0
  170. package/dist/elements/index.d.ts.map +1 -0
  171. package/dist/elements/index.js +3 -0
  172. package/dist/elements/index.js.map +1 -0
  173. package/dist/entries/BadgeEntry.d.ts +21 -0
  174. package/dist/entries/BadgeEntry.d.ts.map +1 -0
  175. package/dist/entries/BadgeEntry.js +32 -0
  176. package/dist/entries/BadgeEntry.js.map +1 -0
  177. package/dist/entries/CodeEntry.d.ts +38 -0
  178. package/dist/entries/CodeEntry.d.ts.map +1 -0
  179. package/dist/entries/CodeEntry.js +44 -0
  180. package/dist/entries/CodeEntry.js.map +1 -0
  181. package/dist/entries/ColorEntry.d.ts +32 -0
  182. package/dist/entries/ColorEntry.d.ts.map +1 -0
  183. package/dist/entries/ColorEntry.js +48 -0
  184. package/dist/entries/ColorEntry.js.map +1 -0
  185. package/dist/entries/ComponentEntry.d.ts +66 -0
  186. package/dist/entries/ComponentEntry.d.ts.map +1 -0
  187. package/dist/entries/ComponentEntry.js +86 -0
  188. package/dist/entries/ComponentEntry.js.map +1 -0
  189. package/dist/entries/Entry.d.ts +175 -0
  190. package/dist/entries/Entry.d.ts.map +1 -0
  191. package/dist/entries/Entry.js +233 -0
  192. package/dist/entries/Entry.js.map +1 -0
  193. package/dist/entries/IconEntry.d.ts +30 -0
  194. package/dist/entries/IconEntry.d.ts.map +1 -0
  195. package/dist/entries/IconEntry.js +34 -0
  196. package/dist/entries/IconEntry.js.map +1 -0
  197. package/dist/entries/ImageEntry.d.ts +33 -0
  198. package/dist/entries/ImageEntry.d.ts.map +1 -0
  199. package/dist/entries/ImageEntry.js +47 -0
  200. package/dist/entries/ImageEntry.js.map +1 -0
  201. package/dist/entries/KeyValueEntry.d.ts +30 -0
  202. package/dist/entries/KeyValueEntry.d.ts.map +1 -0
  203. package/dist/entries/KeyValueEntry.js +38 -0
  204. package/dist/entries/KeyValueEntry.js.map +1 -0
  205. package/dist/entries/RepeatableEntry.d.ts +122 -0
  206. package/dist/entries/RepeatableEntry.d.ts.map +1 -0
  207. package/dist/entries/RepeatableEntry.js +121 -0
  208. package/dist/entries/RepeatableEntry.js.map +1 -0
  209. package/dist/entries/TextEntry.d.ts +38 -0
  210. package/dist/entries/TextEntry.d.ts.map +1 -0
  211. package/dist/entries/TextEntry.js +49 -0
  212. package/dist/entries/TextEntry.js.map +1 -0
  213. package/dist/entries/index.d.ts +2 -0
  214. package/dist/entries/index.d.ts.map +1 -0
  215. package/dist/entries/index.js +8 -0
  216. package/dist/entries/index.js.map +1 -0
  217. package/dist/entries/registry.d.ts +41 -0
  218. package/dist/entries/registry.d.ts.map +1 -0
  219. package/dist/entries/registry.js +17 -0
  220. package/dist/entries/registry.js.map +1 -0
  221. package/dist/fields/BuilderField.d.ts +420 -0
  222. package/dist/fields/BuilderField.d.ts.map +1 -0
  223. package/dist/fields/BuilderField.js +359 -0
  224. package/dist/fields/BuilderField.js.map +1 -0
  225. package/dist/fields/CheckboxField.d.ts +18 -0
  226. package/dist/fields/CheckboxField.d.ts.map +1 -0
  227. package/dist/fields/CheckboxField.js +23 -0
  228. package/dist/fields/CheckboxField.js.map +1 -0
  229. package/dist/fields/CheckboxListField.d.ts +25 -0
  230. package/dist/fields/CheckboxListField.d.ts.map +1 -0
  231. package/dist/fields/CheckboxListField.js +46 -0
  232. package/dist/fields/CheckboxListField.js.map +1 -0
  233. package/dist/fields/ColorPickerField.d.ts +16 -0
  234. package/dist/fields/ColorPickerField.d.ts.map +1 -0
  235. package/dist/fields/ColorPickerField.js +21 -0
  236. package/dist/fields/ColorPickerField.js.map +1 -0
  237. package/dist/fields/DateField.d.ts +29 -0
  238. package/dist/fields/DateField.d.ts.map +1 -0
  239. package/dist/fields/DateField.js +45 -0
  240. package/dist/fields/DateField.js.map +1 -0
  241. package/dist/fields/EmailField.d.ts +8 -0
  242. package/dist/fields/EmailField.d.ts.map +1 -0
  243. package/dist/fields/EmailField.js +13 -0
  244. package/dist/fields/EmailField.js.map +1 -0
  245. package/dist/fields/Field.d.ts +485 -0
  246. package/dist/fields/Field.d.ts.map +1 -0
  247. package/dist/fields/Field.js +539 -0
  248. package/dist/fields/Field.js.map +1 -0
  249. package/dist/fields/FileUploadField.d.ts +43 -0
  250. package/dist/fields/FileUploadField.d.ts.map +1 -0
  251. package/dist/fields/FileUploadField.js +60 -0
  252. package/dist/fields/FileUploadField.js.map +1 -0
  253. package/dist/fields/HiddenField.d.ts +19 -0
  254. package/dist/fields/HiddenField.d.ts.map +1 -0
  255. package/dist/fields/HiddenField.js +24 -0
  256. package/dist/fields/HiddenField.js.map +1 -0
  257. package/dist/fields/KeyValueField.d.ts +36 -0
  258. package/dist/fields/KeyValueField.d.ts.map +1 -0
  259. package/dist/fields/KeyValueField.js +47 -0
  260. package/dist/fields/KeyValueField.js.map +1 -0
  261. package/dist/fields/MarkdownField.d.ts +79 -0
  262. package/dist/fields/MarkdownField.d.ts.map +1 -0
  263. package/dist/fields/MarkdownField.js +117 -0
  264. package/dist/fields/MarkdownField.js.map +1 -0
  265. package/dist/fields/NumberField.d.ts +17 -0
  266. package/dist/fields/NumberField.d.ts.map +1 -0
  267. package/dist/fields/NumberField.js +27 -0
  268. package/dist/fields/NumberField.js.map +1 -0
  269. package/dist/fields/RadioField.d.ts +26 -0
  270. package/dist/fields/RadioField.d.ts.map +1 -0
  271. package/dist/fields/RadioField.js +47 -0
  272. package/dist/fields/RadioField.js.map +1 -0
  273. package/dist/fields/RepeaterField.d.ts +594 -0
  274. package/dist/fields/RepeaterField.d.ts.map +1 -0
  275. package/dist/fields/RepeaterField.js +504 -0
  276. package/dist/fields/RepeaterField.js.map +1 -0
  277. package/dist/fields/RowButton.d.ts +86 -0
  278. package/dist/fields/RowButton.d.ts.map +1 -0
  279. package/dist/fields/RowButton.js +85 -0
  280. package/dist/fields/RowButton.js.map +1 -0
  281. package/dist/fields/SelectField.d.ts +127 -0
  282. package/dist/fields/SelectField.d.ts.map +1 -0
  283. package/dist/fields/SelectField.js +160 -0
  284. package/dist/fields/SelectField.js.map +1 -0
  285. package/dist/fields/SliderField.d.ts +31 -0
  286. package/dist/fields/SliderField.d.ts.map +1 -0
  287. package/dist/fields/SliderField.js +45 -0
  288. package/dist/fields/SliderField.js.map +1 -0
  289. package/dist/fields/SlugField.d.ts +11 -0
  290. package/dist/fields/SlugField.d.ts.map +1 -0
  291. package/dist/fields/SlugField.js +19 -0
  292. package/dist/fields/SlugField.js.map +1 -0
  293. package/dist/fields/TagsInputField.d.ts +65 -0
  294. package/dist/fields/TagsInputField.d.ts.map +1 -0
  295. package/dist/fields/TagsInputField.js +104 -0
  296. package/dist/fields/TagsInputField.js.map +1 -0
  297. package/dist/fields/TextField.d.ts +11 -0
  298. package/dist/fields/TextField.d.ts.map +1 -0
  299. package/dist/fields/TextField.js +19 -0
  300. package/dist/fields/TextField.js.map +1 -0
  301. package/dist/fields/TextareaField.d.ts +40 -0
  302. package/dist/fields/TextareaField.d.ts.map +1 -0
  303. package/dist/fields/TextareaField.js +51 -0
  304. package/dist/fields/TextareaField.js.map +1 -0
  305. package/dist/fields/ToggleButtonsField.d.ts +24 -0
  306. package/dist/fields/ToggleButtonsField.d.ts.map +1 -0
  307. package/dist/fields/ToggleButtonsField.js +41 -0
  308. package/dist/fields/ToggleButtonsField.js.map +1 -0
  309. package/dist/fields/ToggleField.d.ts +8 -0
  310. package/dist/fields/ToggleField.d.ts.map +1 -0
  311. package/dist/fields/ToggleField.js +13 -0
  312. package/dist/fields/ToggleField.js.map +1 -0
  313. package/dist/fields/optionsResolver.d.ts +54 -0
  314. package/dist/fields/optionsResolver.d.ts.map +1 -0
  315. package/dist/fields/optionsResolver.js +62 -0
  316. package/dist/fields/optionsResolver.js.map +1 -0
  317. package/dist/fields/resolveField.d.ts +21 -0
  318. package/dist/fields/resolveField.d.ts.map +1 -0
  319. package/dist/fields/resolveField.js +26 -0
  320. package/dist/fields/resolveField.js.map +1 -0
  321. package/dist/filters/BooleanFilter.d.ts +20 -0
  322. package/dist/filters/BooleanFilter.d.ts.map +1 -0
  323. package/dist/filters/BooleanFilter.js +31 -0
  324. package/dist/filters/BooleanFilter.js.map +1 -0
  325. package/dist/filters/DateRangeFilter.d.ts +68 -0
  326. package/dist/filters/DateRangeFilter.d.ts.map +1 -0
  327. package/dist/filters/DateRangeFilter.js +137 -0
  328. package/dist/filters/DateRangeFilter.js.map +1 -0
  329. package/dist/filters/Filter.d.ts +140 -0
  330. package/dist/filters/Filter.d.ts.map +1 -0
  331. package/dist/filters/Filter.js +99 -0
  332. package/dist/filters/Filter.js.map +1 -0
  333. package/dist/filters/FormFilter.d.ts +103 -0
  334. package/dist/filters/FormFilter.d.ts.map +1 -0
  335. package/dist/filters/FormFilter.js +180 -0
  336. package/dist/filters/FormFilter.js.map +1 -0
  337. package/dist/filters/MultiSelectFilter.d.ts +41 -0
  338. package/dist/filters/MultiSelectFilter.d.ts.map +1 -0
  339. package/dist/filters/MultiSelectFilter.js +67 -0
  340. package/dist/filters/MultiSelectFilter.js.map +1 -0
  341. package/dist/filters/QueryBuilderFilter.d.ts +145 -0
  342. package/dist/filters/QueryBuilderFilter.d.ts.map +1 -0
  343. package/dist/filters/QueryBuilderFilter.js +323 -0
  344. package/dist/filters/QueryBuilderFilter.js.map +1 -0
  345. package/dist/filters/SelectFilter.d.ts +26 -0
  346. package/dist/filters/SelectFilter.d.ts.map +1 -0
  347. package/dist/filters/SelectFilter.js +35 -0
  348. package/dist/filters/SelectFilter.js.map +1 -0
  349. package/dist/filters/TernaryFilter.d.ts +35 -0
  350. package/dist/filters/TernaryFilter.d.ts.map +1 -0
  351. package/dist/filters/TernaryFilter.js +71 -0
  352. package/dist/filters/TernaryFilter.js.map +1 -0
  353. package/dist/filters/TrashedFilter.d.ts +28 -0
  354. package/dist/filters/TrashedFilter.d.ts.map +1 -0
  355. package/dist/filters/TrashedFilter.js +52 -0
  356. package/dist/filters/TrashedFilter.js.map +1 -0
  357. package/dist/filters/queryBuilder/BooleanConstraint.d.ts +13 -0
  358. package/dist/filters/queryBuilder/BooleanConstraint.d.ts.map +1 -0
  359. package/dist/filters/queryBuilder/BooleanConstraint.js +27 -0
  360. package/dist/filters/queryBuilder/BooleanConstraint.js.map +1 -0
  361. package/dist/filters/queryBuilder/Constraint.d.ts +74 -0
  362. package/dist/filters/queryBuilder/Constraint.d.ts.map +1 -0
  363. package/dist/filters/queryBuilder/Constraint.js +45 -0
  364. package/dist/filters/queryBuilder/Constraint.js.map +1 -0
  365. package/dist/filters/queryBuilder/DateConstraint.d.ts +18 -0
  366. package/dist/filters/queryBuilder/DateConstraint.d.ts.map +1 -0
  367. package/dist/filters/queryBuilder/DateConstraint.js +63 -0
  368. package/dist/filters/queryBuilder/DateConstraint.js.map +1 -0
  369. package/dist/filters/queryBuilder/NumberConstraint.d.ts +12 -0
  370. package/dist/filters/queryBuilder/NumberConstraint.d.ts.map +1 -0
  371. package/dist/filters/queryBuilder/NumberConstraint.js +61 -0
  372. package/dist/filters/queryBuilder/NumberConstraint.js.map +1 -0
  373. package/dist/filters/queryBuilder/SelectConstraint.d.ts +22 -0
  374. package/dist/filters/queryBuilder/SelectConstraint.d.ts.map +1 -0
  375. package/dist/filters/queryBuilder/SelectConstraint.js +66 -0
  376. package/dist/filters/queryBuilder/SelectConstraint.js.map +1 -0
  377. package/dist/filters/queryBuilder/TextConstraint.d.ts +18 -0
  378. package/dist/filters/queryBuilder/TextConstraint.d.ts.map +1 -0
  379. package/dist/filters/queryBuilder/TextConstraint.js +58 -0
  380. package/dist/filters/queryBuilder/TextConstraint.js.map +1 -0
  381. package/dist/filters/queryBuilder/index.d.ts +7 -0
  382. package/dist/filters/queryBuilder/index.d.ts.map +1 -0
  383. package/dist/filters/queryBuilder/index.js +7 -0
  384. package/dist/filters/queryBuilder/index.js.map +1 -0
  385. package/dist/icons/index.d.ts +3 -0
  386. package/dist/icons/index.d.ts.map +1 -0
  387. package/dist/icons/index.js +3 -0
  388. package/dist/icons/index.js.map +1 -0
  389. package/dist/icons/lucide.d.ts +16 -0
  390. package/dist/icons/lucide.d.ts.map +1 -0
  391. package/dist/icons/lucide.js +173 -0
  392. package/dist/icons/lucide.js.map +1 -0
  393. package/dist/icons/registry.d.ts +27 -0
  394. package/dist/icons/registry.d.ts.map +1 -0
  395. package/dist/icons/registry.js +35 -0
  396. package/dist/icons/registry.js.map +1 -0
  397. package/dist/icons/types.d.ts +38 -0
  398. package/dist/icons/types.d.ts.map +1 -0
  399. package/dist/icons/types.js +23 -0
  400. package/dist/icons/types.js.map +1 -0
  401. package/dist/index.d.ts +118 -0
  402. package/dist/index.d.ts.map +1 -0
  403. package/dist/index.js +135 -0
  404. package/dist/index.js.map +1 -0
  405. package/dist/io/csv.d.ts +51 -0
  406. package/dist/io/csv.d.ts.map +1 -0
  407. package/dist/io/csv.js +168 -0
  408. package/dist/io/csv.js.map +1 -0
  409. package/dist/notifications/Notification.d.ts +181 -0
  410. package/dist/notifications/Notification.d.ts.map +1 -0
  411. package/dist/notifications/Notification.js +290 -0
  412. package/dist/notifications/Notification.js.map +1 -0
  413. package/dist/notifications/broadcast.d.ts +58 -0
  414. package/dist/notifications/broadcast.d.ts.map +1 -0
  415. package/dist/notifications/broadcast.js +72 -0
  416. package/dist/notifications/broadcast.js.map +1 -0
  417. package/dist/notifications/database.d.ts +164 -0
  418. package/dist/notifications/database.d.ts.map +1 -0
  419. package/dist/notifications/database.js +321 -0
  420. package/dist/notifications/database.js.map +1 -0
  421. package/dist/notifications/dispatchNotificationAction.d.ts +48 -0
  422. package/dist/notifications/dispatchNotificationAction.d.ts.map +1 -0
  423. package/dist/notifications/dispatchNotificationAction.js +100 -0
  424. package/dist/notifications/dispatchNotificationAction.js.map +1 -0
  425. package/dist/notifications/flash.d.ts +34 -0
  426. package/dist/notifications/flash.d.ts.map +1 -0
  427. package/dist/notifications/flash.js +51 -0
  428. package/dist/notifications/flash.js.map +1 -0
  429. package/dist/notifications/index.d.ts +8 -0
  430. package/dist/notifications/index.d.ts.map +1 -0
  431. package/dist/notifications/index.js +6 -0
  432. package/dist/notifications/index.js.map +1 -0
  433. package/dist/notifications/registerBroadcastAuth.d.ts +45 -0
  434. package/dist/notifications/registerBroadcastAuth.d.ts.map +1 -0
  435. package/dist/notifications/registerBroadcastAuth.js +86 -0
  436. package/dist/notifications/registerBroadcastAuth.js.map +1 -0
  437. package/dist/notifications/resolveSavedNotification.d.ts +21 -0
  438. package/dist/notifications/resolveSavedNotification.d.ts.map +1 -0
  439. package/dist/notifications/resolveSavedNotification.js +43 -0
  440. package/dist/notifications/resolveSavedNotification.js.map +1 -0
  441. package/dist/notifications/types.d.ts +87 -0
  442. package/dist/notifications/types.d.ts.map +1 -0
  443. package/dist/notifications/types.js +2 -0
  444. package/dist/notifications/types.js.map +1 -0
  445. package/dist/orm/m2mAccessor.d.ts +49 -0
  446. package/dist/orm/m2mAccessor.d.ts.map +1 -0
  447. package/dist/orm/m2mAccessor.js +45 -0
  448. package/dist/orm/m2mAccessor.js.map +1 -0
  449. package/dist/orm/modelDefaults.d.ts +347 -0
  450. package/dist/orm/modelDefaults.d.ts.map +1 -0
  451. package/dist/orm/modelDefaults.js +375 -0
  452. package/dist/orm/modelDefaults.js.map +1 -0
  453. package/dist/pageData.d.ts +778 -0
  454. package/dist/pageData.d.ts.map +1 -0
  455. package/dist/pageData.js +3725 -0
  456. package/dist/pageData.js.map +1 -0
  457. package/dist/plugins/index.d.ts +2 -0
  458. package/dist/plugins/index.d.ts.map +1 -0
  459. package/dist/plugins/index.js +2 -0
  460. package/dist/plugins/index.js.map +1 -0
  461. package/dist/plugins/themeEditor.d.ts +17 -0
  462. package/dist/plugins/themeEditor.d.ts.map +1 -0
  463. package/dist/plugins/themeEditor.js +23 -0
  464. package/dist/plugins/themeEditor.js.map +1 -0
  465. package/dist/react/AppShell.d.ts +58 -0
  466. package/dist/react/AppShell.d.ts.map +1 -0
  467. package/dist/react/AppShell.js +58 -0
  468. package/dist/react/AppShell.js.map +1 -0
  469. package/dist/react/CommandPalette.d.ts +21 -0
  470. package/dist/react/CommandPalette.d.ts.map +1 -0
  471. package/dist/react/CommandPalette.js +236 -0
  472. package/dist/react/CommandPalette.js.map +1 -0
  473. package/dist/react/FormStateContext.d.ts +83 -0
  474. package/dist/react/FormStateContext.d.ts.map +1 -0
  475. package/dist/react/FormStateContext.js +284 -0
  476. package/dist/react/FormStateContext.js.map +1 -0
  477. package/dist/react/HeadHooks.d.ts +26 -0
  478. package/dist/react/HeadHooks.d.ts.map +1 -0
  479. package/dist/react/HeadHooks.js +141 -0
  480. package/dist/react/HeadHooks.js.map +1 -0
  481. package/dist/react/NotificationActionStrip.d.ts +39 -0
  482. package/dist/react/NotificationActionStrip.d.ts.map +1 -0
  483. package/dist/react/NotificationActionStrip.js +129 -0
  484. package/dist/react/NotificationActionStrip.js.map +1 -0
  485. package/dist/react/NotificationBell.d.ts +20 -0
  486. package/dist/react/NotificationBell.d.ts.map +1 -0
  487. package/dist/react/NotificationBell.js +273 -0
  488. package/dist/react/NotificationBell.js.map +1 -0
  489. package/dist/react/RenderHookSlot.d.ts +20 -0
  490. package/dist/react/RenderHookSlot.d.ts.map +1 -0
  491. package/dist/react/RenderHookSlot.js +24 -0
  492. package/dist/react/RenderHookSlot.js.map +1 -0
  493. package/dist/react/RightSidebar.d.ts +33 -0
  494. package/dist/react/RightSidebar.d.ts.map +1 -0
  495. package/dist/react/RightSidebar.js +82 -0
  496. package/dist/react/RightSidebar.js.map +1 -0
  497. package/dist/react/RightSidebarContext.d.ts +62 -0
  498. package/dist/react/RightSidebarContext.d.ts.map +1 -0
  499. package/dist/react/RightSidebarContext.js +178 -0
  500. package/dist/react/RightSidebarContext.js.map +1 -0
  501. package/dist/react/RightSidebarTrigger.d.ts +16 -0
  502. package/dist/react/RightSidebarTrigger.d.ts.map +1 -0
  503. package/dist/react/RightSidebarTrigger.js +24 -0
  504. package/dist/react/RightSidebarTrigger.js.map +1 -0
  505. package/dist/react/SchemaRenderer.d.ts +63 -0
  506. package/dist/react/SchemaRenderer.d.ts.map +1 -0
  507. package/dist/react/SchemaRenderer.js +3458 -0
  508. package/dist/react/SchemaRenderer.js.map +1 -0
  509. package/dist/react/SearchTrigger.d.ts +13 -0
  510. package/dist/react/SearchTrigger.d.ts.map +1 -0
  511. package/dist/react/SearchTrigger.js +30 -0
  512. package/dist/react/SearchTrigger.js.map +1 -0
  513. package/dist/react/ThemeProvider.d.ts +18 -0
  514. package/dist/react/ThemeProvider.d.ts.map +1 -0
  515. package/dist/react/ThemeProvider.js +66 -0
  516. package/dist/react/ThemeProvider.js.map +1 -0
  517. package/dist/react/ThemeSettingsPage.d.ts +10 -0
  518. package/dist/react/ThemeSettingsPage.d.ts.map +1 -0
  519. package/dist/react/ThemeSettingsPage.js +293 -0
  520. package/dist/react/ThemeSettingsPage.js.map +1 -0
  521. package/dist/react/ThemeToggle.d.ts +2 -0
  522. package/dist/react/ThemeToggle.d.ts.map +1 -0
  523. package/dist/react/ThemeToggle.js +8 -0
  524. package/dist/react/ThemeToggle.js.map +1 -0
  525. package/dist/react/Toaster.d.ts +25 -0
  526. package/dist/react/Toaster.d.ts.map +1 -0
  527. package/dist/react/Toaster.js +89 -0
  528. package/dist/react/Toaster.js.map +1 -0
  529. package/dist/react/UserMenu.d.ts +23 -0
  530. package/dist/react/UserMenu.d.ts.map +1 -0
  531. package/dist/react/UserMenu.js +78 -0
  532. package/dist/react/UserMenu.js.map +1 -0
  533. package/dist/react/WidgetDataContext.d.ts +64 -0
  534. package/dist/react/WidgetDataContext.d.ts.map +1 -0
  535. package/dist/react/WidgetDataContext.js +89 -0
  536. package/dist/react/WidgetDataContext.js.map +1 -0
  537. package/dist/react/cells/EditableCell.d.ts +20 -0
  538. package/dist/react/cells/EditableCell.d.ts.map +1 -0
  539. package/dist/react/cells/EditableCell.js +251 -0
  540. package/dist/react/cells/EditableCell.js.map +1 -0
  541. package/dist/react/fieldJsHandler.d.ts +33 -0
  542. package/dist/react/fieldJsHandler.d.ts.map +1 -0
  543. package/dist/react/fieldJsHandler.js +61 -0
  544. package/dist/react/fieldJsHandler.js.map +1 -0
  545. package/dist/react/fields/BuilderInput.d.ts +21 -0
  546. package/dist/react/fields/BuilderInput.d.ts.map +1 -0
  547. package/dist/react/fields/BuilderInput.js +553 -0
  548. package/dist/react/fields/BuilderInput.js.map +1 -0
  549. package/dist/react/fields/CheckboxInput.d.ts +9 -0
  550. package/dist/react/fields/CheckboxInput.d.ts.map +1 -0
  551. package/dist/react/fields/CheckboxInput.js +23 -0
  552. package/dist/react/fields/CheckboxInput.js.map +1 -0
  553. package/dist/react/fields/CheckboxListInput.d.ts +19 -0
  554. package/dist/react/fields/CheckboxListInput.d.ts.map +1 -0
  555. package/dist/react/fields/CheckboxListInput.js +53 -0
  556. package/dist/react/fields/CheckboxListInput.js.map +1 -0
  557. package/dist/react/fields/ColorInput.d.ts +12 -0
  558. package/dist/react/fields/ColorInput.d.ts.map +1 -0
  559. package/dist/react/fields/ColorInput.js +29 -0
  560. package/dist/react/fields/ColorInput.js.map +1 -0
  561. package/dist/react/fields/DateFieldInput.d.ts +8 -0
  562. package/dist/react/fields/DateFieldInput.d.ts.map +1 -0
  563. package/dist/react/fields/DateFieldInput.js +39 -0
  564. package/dist/react/fields/DateFieldInput.js.map +1 -0
  565. package/dist/react/fields/DateTimeInput.d.ts +13 -0
  566. package/dist/react/fields/DateTimeInput.d.ts.map +1 -0
  567. package/dist/react/fields/DateTimeInput.js +29 -0
  568. package/dist/react/fields/DateTimeInput.js.map +1 -0
  569. package/dist/react/fields/FieldShell.d.ts +23 -0
  570. package/dist/react/fields/FieldShell.d.ts.map +1 -0
  571. package/dist/react/fields/FieldShell.js +46 -0
  572. package/dist/react/fields/FieldShell.js.map +1 -0
  573. package/dist/react/fields/FileUploadInput.d.ts +21 -0
  574. package/dist/react/fields/FileUploadInput.d.ts.map +1 -0
  575. package/dist/react/fields/FileUploadInput.js +120 -0
  576. package/dist/react/fields/FileUploadInput.js.map +1 -0
  577. package/dist/react/fields/HiddenInput.d.ts +11 -0
  578. package/dist/react/fields/HiddenInput.d.ts.map +1 -0
  579. package/dist/react/fields/HiddenInput.js +14 -0
  580. package/dist/react/fields/HiddenInput.js.map +1 -0
  581. package/dist/react/fields/KeyValueInput.d.ts +18 -0
  582. package/dist/react/fields/KeyValueInput.d.ts.map +1 -0
  583. package/dist/react/fields/KeyValueInput.js +122 -0
  584. package/dist/react/fields/KeyValueInput.js.map +1 -0
  585. package/dist/react/fields/MarkdownInput.d.ts +29 -0
  586. package/dist/react/fields/MarkdownInput.d.ts.map +1 -0
  587. package/dist/react/fields/MarkdownInput.js +250 -0
  588. package/dist/react/fields/MarkdownInput.js.map +1 -0
  589. package/dist/react/fields/RadioInput.d.ts +18 -0
  590. package/dist/react/fields/RadioInput.d.ts.map +1 -0
  591. package/dist/react/fields/RadioInput.js +34 -0
  592. package/dist/react/fields/RadioInput.js.map +1 -0
  593. package/dist/react/fields/RepeaterInput.d.ts +92 -0
  594. package/dist/react/fields/RepeaterInput.d.ts.map +1 -0
  595. package/dist/react/fields/RepeaterInput.js +705 -0
  596. package/dist/react/fields/RepeaterInput.js.map +1 -0
  597. package/dist/react/fields/SelectFieldInput.d.ts +23 -0
  598. package/dist/react/fields/SelectFieldInput.d.ts.map +1 -0
  599. package/dist/react/fields/SelectFieldInput.js +146 -0
  600. package/dist/react/fields/SelectFieldInput.js.map +1 -0
  601. package/dist/react/fields/SliderInput.d.ts +16 -0
  602. package/dist/react/fields/SliderInput.d.ts.map +1 -0
  603. package/dist/react/fields/SliderInput.js +37 -0
  604. package/dist/react/fields/SliderInput.js.map +1 -0
  605. package/dist/react/fields/TagsInput.d.ts +27 -0
  606. package/dist/react/fields/TagsInput.d.ts.map +1 -0
  607. package/dist/react/fields/TagsInput.js +189 -0
  608. package/dist/react/fields/TagsInput.js.map +1 -0
  609. package/dist/react/fields/TextLikeInput.d.ts +18 -0
  610. package/dist/react/fields/TextLikeInput.d.ts.map +1 -0
  611. package/dist/react/fields/TextLikeInput.js +46 -0
  612. package/dist/react/fields/TextLikeInput.js.map +1 -0
  613. package/dist/react/fields/ToggleButtonsInput.d.ts +20 -0
  614. package/dist/react/fields/ToggleButtonsInput.d.ts.map +1 -0
  615. package/dist/react/fields/ToggleButtonsInput.js +42 -0
  616. package/dist/react/fields/ToggleButtonsInput.js.map +1 -0
  617. package/dist/react/fields/ToggleFieldInput.d.ts +7 -0
  618. package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -0
  619. package/dist/react/fields/ToggleFieldInput.js +30 -0
  620. package/dist/react/fields/ToggleFieldInput.js.map +1 -0
  621. package/dist/react/fields/rowChromeButton.d.ts +84 -0
  622. package/dist/react/fields/rowChromeButton.d.ts.map +1 -0
  623. package/dist/react/fields/rowChromeButton.js +111 -0
  624. package/dist/react/fields/rowChromeButton.js.map +1 -0
  625. package/dist/react/fields/syncRowGates.d.ts +11 -0
  626. package/dist/react/fields/syncRowGates.d.ts.map +1 -0
  627. package/dist/react/fields/syncRowGates.js +55 -0
  628. package/dist/react/fields/syncRowGates.js.map +1 -0
  629. package/dist/react/formStateHelpers.d.ts +44 -0
  630. package/dist/react/formStateHelpers.d.ts.map +1 -0
  631. package/dist/react/formStateHelpers.js +230 -0
  632. package/dist/react/formStateHelpers.js.map +1 -0
  633. package/dist/react/hooks/use-mobile.d.ts +2 -0
  634. package/dist/react/hooks/use-mobile.d.ts.map +1 -0
  635. package/dist/react/hooks/use-mobile.js +16 -0
  636. package/dist/react/hooks/use-mobile.js.map +1 -0
  637. package/dist/react/icon-context.d.ts +35 -0
  638. package/dist/react/icon-context.d.ts.map +1 -0
  639. package/dist/react/icon-context.js +45 -0
  640. package/dist/react/icon-context.js.map +1 -0
  641. package/dist/react/index.d.ts +26 -0
  642. package/dist/react/index.d.ts.map +1 -0
  643. package/dist/react/index.js +28 -0
  644. package/dist/react/index.js.map +1 -0
  645. package/dist/react/layouts/SidebarLayout.d.ts +3 -0
  646. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -0
  647. package/dist/react/layouts/SidebarLayout.js +85 -0
  648. package/dist/react/layouts/SidebarLayout.js.map +1 -0
  649. package/dist/react/layouts/TopbarLayout.d.ts +3 -0
  650. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -0
  651. package/dist/react/layouts/TopbarLayout.js +103 -0
  652. package/dist/react/layouts/TopbarLayout.js.map +1 -0
  653. package/dist/react/navigate.d.ts +22 -0
  654. package/dist/react/navigate.d.ts.map +1 -0
  655. package/dist/react/navigate.js +30 -0
  656. package/dist/react/navigate.js.map +1 -0
  657. package/dist/react/registry.d.ts +35 -0
  658. package/dist/react/registry.d.ts.map +1 -0
  659. package/dist/react/registry.js +22 -0
  660. package/dist/react/registry.js.map +1 -0
  661. package/dist/react/right-panel-registry.d.ts +32 -0
  662. package/dist/react/right-panel-registry.d.ts.map +1 -0
  663. package/dist/react/right-panel-registry.js +20 -0
  664. package/dist/react/right-panel-registry.js.map +1 -0
  665. package/dist/react/theme-preview/apply.d.ts +11 -0
  666. package/dist/react/theme-preview/apply.d.ts.map +1 -0
  667. package/dist/react/theme-preview/apply.js +93 -0
  668. package/dist/react/theme-preview/apply.js.map +1 -0
  669. package/dist/react/theme-preview/build-html.d.ts +3 -0
  670. package/dist/react/theme-preview/build-html.d.ts.map +1 -0
  671. package/dist/react/theme-preview/build-html.js +437 -0
  672. package/dist/react/theme-preview/build-html.js.map +1 -0
  673. package/dist/react/ui/button.d.ts +9 -0
  674. package/dist/react/ui/button.d.ts.map +1 -0
  675. package/dist/react/ui/button.js +35 -0
  676. package/dist/react/ui/button.js.map +1 -0
  677. package/dist/react/ui/calendar.d.ts +5 -0
  678. package/dist/react/ui/calendar.d.ts.map +1 -0
  679. package/dist/react/ui/calendar.js +34 -0
  680. package/dist/react/ui/calendar.js.map +1 -0
  681. package/dist/react/ui/checkbox.d.ts +4 -0
  682. package/dist/react/ui/checkbox.d.ts.map +1 -0
  683. package/dist/react/ui/checkbox.js +9 -0
  684. package/dist/react/ui/checkbox.js.map +1 -0
  685. package/dist/react/ui/dialog.d.ts +12 -0
  686. package/dist/react/ui/dialog.d.ts.map +1 -0
  687. package/dist/react/ui/dialog.js +34 -0
  688. package/dist/react/ui/dialog.js.map +1 -0
  689. package/dist/react/ui/dropdown-menu.d.ts +12 -0
  690. package/dist/react/ui/dropdown-menu.d.ts.map +1 -0
  691. package/dist/react/ui/dropdown-menu.js +23 -0
  692. package/dist/react/ui/dropdown-menu.js.map +1 -0
  693. package/dist/react/ui/input.d.ts +4 -0
  694. package/dist/react/ui/input.d.ts.map +1 -0
  695. package/dist/react/ui/input.js +8 -0
  696. package/dist/react/ui/input.js.map +1 -0
  697. package/dist/react/ui/label.d.ts +4 -0
  698. package/dist/react/ui/label.d.ts.map +1 -0
  699. package/dist/react/ui/label.js +7 -0
  700. package/dist/react/ui/label.js.map +1 -0
  701. package/dist/react/ui/popover.d.ts +6 -0
  702. package/dist/react/ui/popover.d.ts.map +1 -0
  703. package/dist/react/ui/popover.js +14 -0
  704. package/dist/react/ui/popover.js.map +1 -0
  705. package/dist/react/ui/select.d.ts +17 -0
  706. package/dist/react/ui/select.d.ts.map +1 -0
  707. package/dist/react/ui/select.js +39 -0
  708. package/dist/react/ui/select.js.map +1 -0
  709. package/dist/react/ui/separator.d.ts +4 -0
  710. package/dist/react/ui/separator.d.ts.map +1 -0
  711. package/dist/react/ui/separator.js +9 -0
  712. package/dist/react/ui/separator.js.map +1 -0
  713. package/dist/react/ui/sheet.d.ts +15 -0
  714. package/dist/react/ui/sheet.d.ts.map +1 -0
  715. package/dist/react/ui/sheet.js +37 -0
  716. package/dist/react/ui/sheet.js.map +1 -0
  717. package/dist/react/ui/sidebar.d.ts +64 -0
  718. package/dist/react/ui/sidebar.d.ts.map +1 -0
  719. package/dist/react/ui/sidebar.js +257 -0
  720. package/dist/react/ui/sidebar.js.map +1 -0
  721. package/dist/react/ui/skeleton.d.ts +3 -0
  722. package/dist/react/ui/skeleton.d.ts.map +1 -0
  723. package/dist/react/ui/skeleton.js +7 -0
  724. package/dist/react/ui/skeleton.js.map +1 -0
  725. package/dist/react/ui/slider.d.ts +4 -0
  726. package/dist/react/ui/slider.d.ts.map +1 -0
  727. package/dist/react/ui/slider.js +8 -0
  728. package/dist/react/ui/slider.js.map +1 -0
  729. package/dist/react/ui/switch.d.ts +4 -0
  730. package/dist/react/ui/switch.d.ts.map +1 -0
  731. package/dist/react/ui/switch.js +8 -0
  732. package/dist/react/ui/switch.js.map +1 -0
  733. package/dist/react/ui/table.d.ts +11 -0
  734. package/dist/react/ui/table.d.ts.map +1 -0
  735. package/dist/react/ui/table.js +28 -0
  736. package/dist/react/ui/table.js.map +1 -0
  737. package/dist/react/ui/tabs.d.ts +7 -0
  738. package/dist/react/ui/tabs.d.ts.map +1 -0
  739. package/dist/react/ui/tabs.js +17 -0
  740. package/dist/react/ui/tabs.js.map +1 -0
  741. package/dist/react/ui/textarea.d.ts +4 -0
  742. package/dist/react/ui/textarea.d.ts.map +1 -0
  743. package/dist/react/ui/textarea.js +7 -0
  744. package/dist/react/ui/textarea.js.map +1 -0
  745. package/dist/react/ui/tooltip.d.ts +7 -0
  746. package/dist/react/ui/tooltip.d.ts.map +1 -0
  747. package/dist/react/ui/tooltip.js +17 -0
  748. package/dist/react/ui/tooltip.js.map +1 -0
  749. package/dist/react/useResizableWidth.d.ts +47 -0
  750. package/dist/react/useResizableWidth.d.ts.map +1 -0
  751. package/dist/react/useResizableWidth.js +99 -0
  752. package/dist/react/useResizableWidth.js.map +1 -0
  753. package/dist/react/utils.d.ts +3 -0
  754. package/dist/react/utils.d.ts.map +1 -0
  755. package/dist/react/utils.js +6 -0
  756. package/dist/react/utils.js.map +1 -0
  757. package/dist/react/widgetRegistry.d.ts +33 -0
  758. package/dist/react/widgetRegistry.d.ts.map +1 -0
  759. package/dist/react/widgetRegistry.js +15 -0
  760. package/dist/react/widgetRegistry.js.map +1 -0
  761. package/dist/react/widgets/StatsOverviewRenderer.d.ts +6 -0
  762. package/dist/react/widgets/StatsOverviewRenderer.d.ts.map +1 -0
  763. package/dist/react/widgets/StatsOverviewRenderer.js +124 -0
  764. package/dist/react/widgets/StatsOverviewRenderer.js.map +1 -0
  765. package/dist/react/widgets/TableWidgetRenderer.d.ts +6 -0
  766. package/dist/react/widgets/TableWidgetRenderer.d.ts.map +1 -0
  767. package/dist/react/widgets/TableWidgetRenderer.js +123 -0
  768. package/dist/react/widgets/TableWidgetRenderer.js.map +1 -0
  769. package/dist/react/widgets/ViewRenderer.d.ts +16 -0
  770. package/dist/react/widgets/ViewRenderer.d.ts.map +1 -0
  771. package/dist/react/widgets/ViewRenderer.js +26 -0
  772. package/dist/react/widgets/ViewRenderer.js.map +1 -0
  773. package/dist/richtext/index.d.ts +2 -0
  774. package/dist/richtext/index.d.ts.map +1 -0
  775. package/dist/richtext/index.js +2 -0
  776. package/dist/richtext/index.js.map +1 -0
  777. package/dist/richtext/registry.d.ts +55 -0
  778. package/dist/richtext/registry.d.ts.map +1 -0
  779. package/dist/richtext/registry.js +66 -0
  780. package/dist/richtext/registry.js.map +1 -0
  781. package/dist/routes.d.ts +9 -0
  782. package/dist/routes.d.ts.map +1 -0
  783. package/dist/routes.js +3116 -0
  784. package/dist/routes.js.map +1 -0
  785. package/dist/schema/Alert.d.ts +33 -0
  786. package/dist/schema/Alert.d.ts.map +1 -0
  787. package/dist/schema/Alert.js +41 -0
  788. package/dist/schema/Alert.js.map +1 -0
  789. package/dist/schema/Block.d.ts +112 -0
  790. package/dist/schema/Block.d.ts.map +1 -0
  791. package/dist/schema/Block.js +136 -0
  792. package/dist/schema/Block.js.map +1 -0
  793. package/dist/schema/Breadcrumbs.d.ts +31 -0
  794. package/dist/schema/Breadcrumbs.d.ts.map +1 -0
  795. package/dist/schema/Breadcrumbs.js +30 -0
  796. package/dist/schema/Breadcrumbs.js.map +1 -0
  797. package/dist/schema/Card.d.ts +17 -0
  798. package/dist/schema/Card.d.ts.map +1 -0
  799. package/dist/schema/Card.js +31 -0
  800. package/dist/schema/Card.js.map +1 -0
  801. package/dist/schema/Divider.d.ts +12 -0
  802. package/dist/schema/Divider.d.ts.map +1 -0
  803. package/dist/schema/Divider.js +19 -0
  804. package/dist/schema/Divider.js.map +1 -0
  805. package/dist/schema/Element.d.ts +150 -0
  806. package/dist/schema/Element.d.ts.map +1 -0
  807. package/dist/schema/Element.js +124 -0
  808. package/dist/schema/Element.js.map +1 -0
  809. package/dist/schema/EmptyState.d.ts +48 -0
  810. package/dist/schema/EmptyState.d.ts.map +1 -0
  811. package/dist/schema/EmptyState.js +57 -0
  812. package/dist/schema/EmptyState.js.map +1 -0
  813. package/dist/schema/Fieldset.d.ts +25 -0
  814. package/dist/schema/Fieldset.d.ts.map +1 -0
  815. package/dist/schema/Fieldset.js +39 -0
  816. package/dist/schema/Fieldset.js.map +1 -0
  817. package/dist/schema/Grid.d.ts +23 -0
  818. package/dist/schema/Grid.d.ts.map +1 -0
  819. package/dist/schema/Grid.js +36 -0
  820. package/dist/schema/Grid.js.map +1 -0
  821. package/dist/schema/Group.d.ts +19 -0
  822. package/dist/schema/Group.d.ts.map +1 -0
  823. package/dist/schema/Group.js +26 -0
  824. package/dist/schema/Group.js.map +1 -0
  825. package/dist/schema/Heading.d.ts +25 -0
  826. package/dist/schema/Heading.d.ts.map +1 -0
  827. package/dist/schema/Heading.js +34 -0
  828. package/dist/schema/Heading.js.map +1 -0
  829. package/dist/schema/Html.d.ts +48 -0
  830. package/dist/schema/Html.d.ts.map +1 -0
  831. package/dist/schema/Html.js +60 -0
  832. package/dist/schema/Html.js.map +1 -0
  833. package/dist/schema/Icon.d.ts +34 -0
  834. package/dist/schema/Icon.d.ts.map +1 -0
  835. package/dist/schema/Icon.js +40 -0
  836. package/dist/schema/Icon.js.map +1 -0
  837. package/dist/schema/Image.d.ts +38 -0
  838. package/dist/schema/Image.d.ts.map +1 -0
  839. package/dist/schema/Image.js +48 -0
  840. package/dist/schema/Image.js.map +1 -0
  841. package/dist/schema/LinkTag.d.ts +48 -0
  842. package/dist/schema/LinkTag.d.ts.map +1 -0
  843. package/dist/schema/LinkTag.js +16 -0
  844. package/dist/schema/LinkTag.js.map +1 -0
  845. package/dist/schema/Markdown.d.ts +57 -0
  846. package/dist/schema/Markdown.d.ts.map +1 -0
  847. package/dist/schema/Markdown.js +75 -0
  848. package/dist/schema/Markdown.js.map +1 -0
  849. package/dist/schema/MetaTag.d.ts +41 -0
  850. package/dist/schema/MetaTag.d.ts.map +1 -0
  851. package/dist/schema/MetaTag.js +16 -0
  852. package/dist/schema/MetaTag.js.map +1 -0
  853. package/dist/schema/RelationTabs.d.ts +50 -0
  854. package/dist/schema/RelationTabs.d.ts.map +1 -0
  855. package/dist/schema/RelationTabs.js +48 -0
  856. package/dist/schema/RelationTabs.js.map +1 -0
  857. package/dist/schema/ScriptTag.d.ts +63 -0
  858. package/dist/schema/ScriptTag.d.ts.map +1 -0
  859. package/dist/schema/ScriptTag.js +16 -0
  860. package/dist/schema/ScriptTag.js.map +1 -0
  861. package/dist/schema/Section.d.ts +93 -0
  862. package/dist/schema/Section.d.ts.map +1 -0
  863. package/dist/schema/Section.js +127 -0
  864. package/dist/schema/Section.js.map +1 -0
  865. package/dist/schema/ServerDataElement.d.ts +101 -0
  866. package/dist/schema/ServerDataElement.d.ts.map +1 -0
  867. package/dist/schema/ServerDataElement.js +135 -0
  868. package/dist/schema/ServerDataElement.js.map +1 -0
  869. package/dist/schema/Split.d.ts +31 -0
  870. package/dist/schema/Split.d.ts.map +1 -0
  871. package/dist/schema/Split.js +41 -0
  872. package/dist/schema/Split.js.map +1 -0
  873. package/dist/schema/Stat.d.ts +92 -0
  874. package/dist/schema/Stat.d.ts.map +1 -0
  875. package/dist/schema/Stat.js +116 -0
  876. package/dist/schema/Stat.js.map +1 -0
  877. package/dist/schema/StatsOverview.d.ts +76 -0
  878. package/dist/schema/StatsOverview.d.ts.map +1 -0
  879. package/dist/schema/StatsOverview.js +71 -0
  880. package/dist/schema/StatsOverview.js.map +1 -0
  881. package/dist/schema/StyleTag.d.ts +32 -0
  882. package/dist/schema/StyleTag.d.ts.map +1 -0
  883. package/dist/schema/StyleTag.js +38 -0
  884. package/dist/schema/StyleTag.js.map +1 -0
  885. package/dist/schema/TableWidget.d.ts +148 -0
  886. package/dist/schema/TableWidget.d.ts.map +1 -0
  887. package/dist/schema/TableWidget.js +190 -0
  888. package/dist/schema/TableWidget.js.map +1 -0
  889. package/dist/schema/Tabs.d.ts +40 -0
  890. package/dist/schema/Tabs.d.ts.map +1 -0
  891. package/dist/schema/Tabs.js +66 -0
  892. package/dist/schema/Tabs.js.map +1 -0
  893. package/dist/schema/Text.d.ts +33 -0
  894. package/dist/schema/Text.d.ts.map +1 -0
  895. package/dist/schema/Text.js +40 -0
  896. package/dist/schema/Text.js.map +1 -0
  897. package/dist/schema/UnorderedList.d.ts +36 -0
  898. package/dist/schema/UnorderedList.d.ts.map +1 -0
  899. package/dist/schema/UnorderedList.js +42 -0
  900. package/dist/schema/UnorderedList.js.map +1 -0
  901. package/dist/schema/View.d.ts +81 -0
  902. package/dist/schema/View.d.ts.map +1 -0
  903. package/dist/schema/View.js +81 -0
  904. package/dist/schema/View.js.map +1 -0
  905. package/dist/schema/Wizard.d.ts +67 -0
  906. package/dist/schema/Wizard.d.ts.map +1 -0
  907. package/dist/schema/Wizard.js +94 -0
  908. package/dist/schema/Wizard.js.map +1 -0
  909. package/dist/schema/index.d.ts +26 -0
  910. package/dist/schema/index.d.ts.map +1 -0
  911. package/dist/schema/index.js +26 -0
  912. package/dist/schema/index.js.map +1 -0
  913. package/dist/schema/resolveSchema.d.ts +122 -0
  914. package/dist/schema/resolveSchema.d.ts.map +1 -0
  915. package/dist/schema/resolveSchema.js +648 -0
  916. package/dist/schema/resolveSchema.js.map +1 -0
  917. package/dist/schema/sanitize.d.ts +21 -0
  918. package/dist/schema/sanitize.d.ts.map +1 -0
  919. package/dist/schema/sanitize.js +46 -0
  920. package/dist/schema/sanitize.js.map +1 -0
  921. package/dist/search.d.ts +53 -0
  922. package/dist/search.d.ts.map +1 -0
  923. package/dist/search.js +114 -0
  924. package/dist/search.js.map +1 -0
  925. package/dist/sessionFilters.d.ts +8 -0
  926. package/dist/sessionFilters.d.ts.map +1 -0
  927. package/dist/sessionFilters.js +115 -0
  928. package/dist/sessionFilters.js.map +1 -0
  929. package/dist/summarizers/Summarizer.d.ts +65 -0
  930. package/dist/summarizers/Summarizer.d.ts.map +1 -0
  931. package/dist/summarizers/Summarizer.js +98 -0
  932. package/dist/summarizers/Summarizer.js.map +1 -0
  933. package/dist/summarizers/index.d.ts +2 -0
  934. package/dist/summarizers/index.d.ts.map +1 -0
  935. package/dist/summarizers/index.js +2 -0
  936. package/dist/summarizers/index.js.map +1 -0
  937. package/dist/theme/base-colors.d.ts +3 -0
  938. package/dist/theme/base-colors.d.ts.map +1 -0
  939. package/dist/theme/base-colors.js +64 -0
  940. package/dist/theme/base-colors.js.map +1 -0
  941. package/dist/theme/chart-colors.d.ts +3 -0
  942. package/dist/theme/chart-colors.d.ts.map +1 -0
  943. package/dist/theme/chart-colors.js +46 -0
  944. package/dist/theme/chart-colors.js.map +1 -0
  945. package/dist/theme/colors.d.ts +56 -0
  946. package/dist/theme/colors.d.ts.map +1 -0
  947. package/dist/theme/colors.js +410 -0
  948. package/dist/theme/colors.js.map +1 -0
  949. package/dist/theme/generate-css.d.ts +9 -0
  950. package/dist/theme/generate-css.d.ts.map +1 -0
  951. package/dist/theme/generate-css.js +36 -0
  952. package/dist/theme/generate-css.js.map +1 -0
  953. package/dist/theme/generate-scale.d.ts +3 -0
  954. package/dist/theme/generate-scale.d.ts.map +1 -0
  955. package/dist/theme/generate-scale.js +89 -0
  956. package/dist/theme/generate-scale.js.map +1 -0
  957. package/dist/theme/icon-map.d.ts +9 -0
  958. package/dist/theme/icon-map.d.ts.map +1 -0
  959. package/dist/theme/icon-map.js +40 -0
  960. package/dist/theme/icon-map.js.map +1 -0
  961. package/dist/theme/index.d.ts +15 -0
  962. package/dist/theme/index.d.ts.map +1 -0
  963. package/dist/theme/index.js +13 -0
  964. package/dist/theme/index.js.map +1 -0
  965. package/dist/theme/migrate.d.ts +14 -0
  966. package/dist/theme/migrate.d.ts.map +1 -0
  967. package/dist/theme/migrate.js +79 -0
  968. package/dist/theme/migrate.js.map +1 -0
  969. package/dist/theme/presets.d.ts +30 -0
  970. package/dist/theme/presets.d.ts.map +1 -0
  971. package/dist/theme/presets.js +128 -0
  972. package/dist/theme/presets.js.map +1 -0
  973. package/dist/theme/radius.d.ts +11 -0
  974. package/dist/theme/radius.d.ts.map +1 -0
  975. package/dist/theme/radius.js +17 -0
  976. package/dist/theme/radius.js.map +1 -0
  977. package/dist/theme/resolve.d.ts +13 -0
  978. package/dist/theme/resolve.d.ts.map +1 -0
  979. package/dist/theme/resolve.js +91 -0
  980. package/dist/theme/resolve.js.map +1 -0
  981. package/dist/theme/spacing.d.ts +14 -0
  982. package/dist/theme/spacing.d.ts.map +1 -0
  983. package/dist/theme/spacing.js +17 -0
  984. package/dist/theme/spacing.js.map +1 -0
  985. package/dist/theme/theme-colors.d.ts +9 -0
  986. package/dist/theme/theme-colors.d.ts.map +1 -0
  987. package/dist/theme/theme-colors.js +84 -0
  988. package/dist/theme/theme-colors.js.map +1 -0
  989. package/dist/theme/types.d.ts +94 -0
  990. package/dist/theme/types.d.ts.map +1 -0
  991. package/dist/theme/types.js +2 -0
  992. package/dist/theme/types.js.map +1 -0
  993. package/dist/uploads/UploadAdapter.d.ts +34 -0
  994. package/dist/uploads/UploadAdapter.d.ts.map +1 -0
  995. package/dist/uploads/UploadAdapter.js +2 -0
  996. package/dist/uploads/UploadAdapter.js.map +1 -0
  997. package/dist/uploads/index.d.ts +3 -0
  998. package/dist/uploads/index.d.ts.map +1 -0
  999. package/dist/uploads/index.js +2 -0
  1000. package/dist/uploads/index.js.map +1 -0
  1001. package/dist/uploads/localUpload.d.ts +25 -0
  1002. package/dist/uploads/localUpload.d.ts.map +1 -0
  1003. package/dist/uploads/localUpload.js +65 -0
  1004. package/dist/uploads/localUpload.js.map +1 -0
  1005. package/dist/validation/Validator.d.ts +40 -0
  1006. package/dist/validation/Validator.d.ts.map +1 -0
  1007. package/dist/validation/Validator.js +25 -0
  1008. package/dist/validation/Validator.js.map +1 -0
  1009. package/dist/validation/index.d.ts +5 -0
  1010. package/dist/validation/index.d.ts.map +1 -0
  1011. package/dist/validation/index.js +5 -0
  1012. package/dist/validation/index.js.map +1 -0
  1013. package/dist/validation/rules.d.ts +9 -0
  1014. package/dist/validation/rules.d.ts.map +1 -0
  1015. package/dist/validation/rules.js +61 -0
  1016. package/dist/validation/rules.js.map +1 -0
  1017. package/dist/validation/runValidators.d.ts +30 -0
  1018. package/dist/validation/runValidators.d.ts.map +1 -0
  1019. package/dist/validation/runValidators.js +438 -0
  1020. package/dist/validation/runValidators.js.map +1 -0
  1021. package/dist/validation/uniqueValidator.d.ts +61 -0
  1022. package/dist/validation/uniqueValidator.d.ts.map +1 -0
  1023. package/dist/validation/uniqueValidator.js +80 -0
  1024. package/dist/validation/uniqueValidator.js.map +1 -0
  1025. package/dist/vite.d.ts +19 -0
  1026. package/dist/vite.d.ts.map +1 -0
  1027. package/dist/vite.js +696 -0
  1028. package/dist/vite.js.map +1 -0
  1029. package/dist/widgets/index.d.ts +2 -0
  1030. package/dist/widgets/index.d.ts.map +1 -0
  1031. package/dist/widgets/index.js +7 -0
  1032. package/dist/widgets/index.js.map +1 -0
  1033. package/dist/widgets/registry.d.ts +32 -0
  1034. package/dist/widgets/registry.d.ts.map +1 -0
  1035. package/dist/widgets/registry.js +17 -0
  1036. package/dist/widgets/registry.js.map +1 -0
  1037. package/package.json +101 -0
  1038. package/src/Cluster.test.ts +283 -0
  1039. package/src/Cluster.ts +83 -0
  1040. package/src/Column.test.ts +140 -0
  1041. package/src/Column.ts +612 -0
  1042. package/src/Global.test.ts +367 -0
  1043. package/src/Global.ts +169 -0
  1044. package/src/Page.test.ts +50 -0
  1045. package/src/Page.ts +139 -0
  1046. package/src/Pilotiq.test.ts +47 -0
  1047. package/src/Pilotiq.ts +705 -0
  1048. package/src/PilotiqRegistry.ts +36 -0
  1049. package/src/PilotiqServiceProvider.ts +69 -0
  1050. package/src/RelationManager.test.ts +400 -0
  1051. package/src/RelationManager.ts +527 -0
  1052. package/src/RenderHook.test.ts +252 -0
  1053. package/src/RenderHook.ts +226 -0
  1054. package/src/Resource.test.ts +240 -0
  1055. package/src/Resource.ts +439 -0
  1056. package/src/RightPanel.test.ts +202 -0
  1057. package/src/RightPanel.ts +132 -0
  1058. package/src/Tab.test.ts +91 -0
  1059. package/src/Tab.ts +156 -0
  1060. package/src/UserMenuItem.ts +145 -0
  1061. package/src/actions/Action.test.ts +2479 -0
  1062. package/src/actions/Action.ts +2124 -0
  1063. package/src/actions/ActionGroup.test.ts +112 -0
  1064. package/src/actions/ActionGroup.ts +173 -0
  1065. package/src/actions/attachFactory.ts +172 -0
  1066. package/src/actions/exportFactory.ts +215 -0
  1067. package/src/actions/importFactory.ts +222 -0
  1068. package/src/actions/index.ts +17 -0
  1069. package/src/applyPageHooks.test.ts +298 -0
  1070. package/src/applyPageHooks.ts +242 -0
  1071. package/src/authorization.test.ts +483 -0
  1072. package/src/breadcrumbs.test.ts +238 -0
  1073. package/src/cells/coerce.test.ts +85 -0
  1074. package/src/cells/coerce.ts +84 -0
  1075. package/src/clusterPaths.ts +35 -0
  1076. package/src/columns/BadgeColumn.test.ts +54 -0
  1077. package/src/columns/BadgeColumn.ts +32 -0
  1078. package/src/columns/BooleanColumn.test.ts +41 -0
  1079. package/src/columns/BooleanColumn.ts +18 -0
  1080. package/src/columns/ColorColumn.test.ts +37 -0
  1081. package/src/columns/ColorColumn.ts +38 -0
  1082. package/src/columns/IconColumn.test.ts +54 -0
  1083. package/src/columns/IconColumn.ts +37 -0
  1084. package/src/columns/ImageColumn.test.ts +41 -0
  1085. package/src/columns/ImageColumn.ts +28 -0
  1086. package/src/columns/SelectColumn.ts +60 -0
  1087. package/src/columns/TextColumn.test.ts +190 -0
  1088. package/src/columns/TextColumn.ts +20 -0
  1089. package/src/columns/TextInputColumn.ts +68 -0
  1090. package/src/columns/ToggleColumn.ts +46 -0
  1091. package/src/columns/editableColumns.test.ts +193 -0
  1092. package/src/columns/index.ts +9 -0
  1093. package/src/defaultGlobalPages.ts +95 -0
  1094. package/src/defaultPages.test.ts +634 -0
  1095. package/src/defaultPages.ts +614 -0
  1096. package/src/defaultViewPage.test.ts +147 -0
  1097. package/src/elements/Form.test.ts +223 -0
  1098. package/src/elements/Form.ts +397 -0
  1099. package/src/elements/ListTabs.ts +28 -0
  1100. package/src/elements/Table.test.ts +422 -0
  1101. package/src/elements/Table.ts +816 -0
  1102. package/src/elements/TableGroup.test.ts +149 -0
  1103. package/src/elements/TableGroup.ts +199 -0
  1104. package/src/elements/dispatchAction.test.ts +463 -0
  1105. package/src/elements/dispatchAction.ts +355 -0
  1106. package/src/elements/dispatchForm.test.ts +455 -0
  1107. package/src/elements/dispatchForm.ts +1855 -0
  1108. package/src/elements/dispatchTable.test.ts +1247 -0
  1109. package/src/elements/dispatchTable.ts +666 -0
  1110. package/src/elements/index.ts +21 -0
  1111. package/src/entries/BadgeEntry.ts +39 -0
  1112. package/src/entries/CodeEntry.test.ts +40 -0
  1113. package/src/entries/CodeEntry.ts +52 -0
  1114. package/src/entries/ColorEntry.ts +63 -0
  1115. package/src/entries/ComponentEntry.test.ts +173 -0
  1116. package/src/entries/ComponentEntry.ts +95 -0
  1117. package/src/entries/Entry.ts +304 -0
  1118. package/src/entries/IconEntry.ts +49 -0
  1119. package/src/entries/ImageEntry.ts +61 -0
  1120. package/src/entries/KeyValueEntry.ts +47 -0
  1121. package/src/entries/RepeatableEntry.test.ts +239 -0
  1122. package/src/entries/RepeatableEntry.ts +173 -0
  1123. package/src/entries/TextEntry.test.ts +394 -0
  1124. package/src/entries/TextEntry.ts +60 -0
  1125. package/src/entries/index.ts +12 -0
  1126. package/src/entries/leaves.test.ts +306 -0
  1127. package/src/entries/registry.ts +54 -0
  1128. package/src/fields/BuilderField.test.ts +1188 -0
  1129. package/src/fields/BuilderField.ts +568 -0
  1130. package/src/fields/BuilderRelationship.test.ts +811 -0
  1131. package/src/fields/CheckboxField.test.ts +44 -0
  1132. package/src/fields/CheckboxField.ts +27 -0
  1133. package/src/fields/CheckboxListField.test.ts +99 -0
  1134. package/src/fields/CheckboxListField.ts +66 -0
  1135. package/src/fields/ColorPickerField.test.ts +33 -0
  1136. package/src/fields/ColorPickerField.ts +25 -0
  1137. package/src/fields/DateField.ts +54 -0
  1138. package/src/fields/DateTimeField.test.ts +55 -0
  1139. package/src/fields/EmailField.ts +16 -0
  1140. package/src/fields/Field.test.ts +639 -0
  1141. package/src/fields/Field.ts +773 -0
  1142. package/src/fields/FileUploadField.test.ts +97 -0
  1143. package/src/fields/FileUploadField.ts +71 -0
  1144. package/src/fields/HiddenField.test.ts +27 -0
  1145. package/src/fields/HiddenField.ts +28 -0
  1146. package/src/fields/KeyValueField.test.ts +105 -0
  1147. package/src/fields/KeyValueField.ts +55 -0
  1148. package/src/fields/MarkdownField.test.ts +167 -0
  1149. package/src/fields/MarkdownField.ts +151 -0
  1150. package/src/fields/NumberField.ts +33 -0
  1151. package/src/fields/RadioField.test.ts +94 -0
  1152. package/src/fields/RadioField.ts +67 -0
  1153. package/src/fields/RepeaterField.test.ts +1806 -0
  1154. package/src/fields/RepeaterField.ts +791 -0
  1155. package/src/fields/RepeaterRelationship.test.ts +1630 -0
  1156. package/src/fields/RepeaterSimple.test.ts +248 -0
  1157. package/src/fields/RowButton.test.ts +149 -0
  1158. package/src/fields/RowButton.ts +125 -0
  1159. package/src/fields/SelectField.test.ts +192 -0
  1160. package/src/fields/SelectField.ts +235 -0
  1161. package/src/fields/SliderField.test.ts +50 -0
  1162. package/src/fields/SliderField.ts +53 -0
  1163. package/src/fields/SlugField.ts +24 -0
  1164. package/src/fields/TagsInputField.test.ts +154 -0
  1165. package/src/fields/TagsInputField.ts +133 -0
  1166. package/src/fields/TextField.ts +24 -0
  1167. package/src/fields/TextareaField.test.ts +58 -0
  1168. package/src/fields/TextareaField.ts +59 -0
  1169. package/src/fields/ToggleButtonsField.test.ts +106 -0
  1170. package/src/fields/ToggleButtonsField.ts +59 -0
  1171. package/src/fields/ToggleField.ts +16 -0
  1172. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +319 -0
  1173. package/src/fields/optionsResolver.ts +95 -0
  1174. package/src/fields/resolveField.ts +28 -0
  1175. package/src/filters/BooleanFilter.ts +35 -0
  1176. package/src/filters/DateRangeFilter.test.ts +194 -0
  1177. package/src/filters/DateRangeFilter.ts +148 -0
  1178. package/src/filters/Filter.test.ts +268 -0
  1179. package/src/filters/Filter.ts +184 -0
  1180. package/src/filters/FormFilter.test.ts +238 -0
  1181. package/src/filters/FormFilter.ts +215 -0
  1182. package/src/filters/MultiSelectFilter.test.ts +119 -0
  1183. package/src/filters/MultiSelectFilter.ts +78 -0
  1184. package/src/filters/QueryBuilderFilter.test.ts +644 -0
  1185. package/src/filters/QueryBuilderFilter.ts +398 -0
  1186. package/src/filters/SelectFilter.ts +46 -0
  1187. package/src/filters/TernaryFilter.test.ts +160 -0
  1188. package/src/filters/TernaryFilter.ts +72 -0
  1189. package/src/filters/TrashedFilter.test.ts +149 -0
  1190. package/src/filters/TrashedFilter.ts +55 -0
  1191. package/src/filters/queryBuilder/BooleanConstraint.ts +31 -0
  1192. package/src/filters/queryBuilder/Constraint.ts +115 -0
  1193. package/src/filters/queryBuilder/DateConstraint.ts +69 -0
  1194. package/src/filters/queryBuilder/NumberConstraint.ts +66 -0
  1195. package/src/filters/queryBuilder/SelectConstraint.ts +72 -0
  1196. package/src/filters/queryBuilder/TextConstraint.ts +65 -0
  1197. package/src/filters/queryBuilder/index.ts +12 -0
  1198. package/src/icons/index.ts +2 -0
  1199. package/src/icons/lucide.ts +204 -0
  1200. package/src/icons/registry.test.ts +56 -0
  1201. package/src/icons/registry.ts +41 -0
  1202. package/src/icons/types.ts +47 -0
  1203. package/src/index.ts +521 -0
  1204. package/src/io/csv.test.ts +142 -0
  1205. package/src/io/csv.ts +170 -0
  1206. package/src/nestedRelationManagerData.test.ts +526 -0
  1207. package/src/notifications/Notification.test.ts +210 -0
  1208. package/src/notifications/Notification.ts +354 -0
  1209. package/src/notifications/broadcast.test.ts +110 -0
  1210. package/src/notifications/broadcast.ts +95 -0
  1211. package/src/notifications/database.test.ts +383 -0
  1212. package/src/notifications/database.ts +398 -0
  1213. package/src/notifications/databaseNotifications.test.ts +187 -0
  1214. package/src/notifications/dispatchNotificationAction.test.ts +341 -0
  1215. package/src/notifications/dispatchNotificationAction.ts +142 -0
  1216. package/src/notifications/flash.test.ts +89 -0
  1217. package/src/notifications/flash.ts +71 -0
  1218. package/src/notifications/index.ts +45 -0
  1219. package/src/notifications/registerBroadcastAuth.test.ts +134 -0
  1220. package/src/notifications/registerBroadcastAuth.ts +100 -0
  1221. package/src/notifications/resolveSavedNotification.test.ts +82 -0
  1222. package/src/notifications/resolveSavedNotification.ts +59 -0
  1223. package/src/notifications/types.ts +93 -0
  1224. package/src/orm/m2mAccessor.ts +66 -0
  1225. package/src/orm/modelDefaults.test.ts +633 -0
  1226. package/src/orm/modelDefaults.ts +632 -0
  1227. package/src/pageData.test.ts +1121 -0
  1228. package/src/pageData.ts +4662 -0
  1229. package/src/plugins/index.ts +1 -0
  1230. package/src/plugins/themeEditor.ts +24 -0
  1231. package/src/react/AppShell.tsx +148 -0
  1232. package/src/react/CommandPalette.tsx +375 -0
  1233. package/src/react/FormStateContext.tsx +398 -0
  1234. package/src/react/HeadHooks.tsx +126 -0
  1235. package/src/react/NotificationActionStrip.tsx +263 -0
  1236. package/src/react/NotificationBell.tsx +426 -0
  1237. package/src/react/RenderHookSlot.tsx +32 -0
  1238. package/src/react/RightSidebar.tsx +257 -0
  1239. package/src/react/RightSidebarContext.tsx +211 -0
  1240. package/src/react/RightSidebarTrigger.tsx +53 -0
  1241. package/src/react/SchemaRenderer.tsx +6128 -0
  1242. package/src/react/SearchTrigger.tsx +46 -0
  1243. package/src/react/ThemeProvider.tsx +93 -0
  1244. package/src/react/ThemeSettingsPage.tsx +579 -0
  1245. package/src/react/ThemeToggle.tsx +20 -0
  1246. package/src/react/Toaster.tsx +158 -0
  1247. package/src/react/UserMenu.tsx +196 -0
  1248. package/src/react/WidgetDataContext.tsx +157 -0
  1249. package/src/react/cells/EditableCell.tsx +376 -0
  1250. package/src/react/fieldJsHandler.test.ts +166 -0
  1251. package/src/react/fieldJsHandler.ts +79 -0
  1252. package/src/react/fields/BuilderInput.tsx +995 -0
  1253. package/src/react/fields/CheckboxInput.tsx +39 -0
  1254. package/src/react/fields/CheckboxListInput.tsx +81 -0
  1255. package/src/react/fields/ColorInput.tsx +51 -0
  1256. package/src/react/fields/DateFieldInput.tsx +70 -0
  1257. package/src/react/fields/DateTimeInput.tsx +42 -0
  1258. package/src/react/fields/FieldShell.tsx +107 -0
  1259. package/src/react/fields/FileUploadInput.tsx +189 -0
  1260. package/src/react/fields/HiddenInput.tsx +17 -0
  1261. package/src/react/fields/KeyValueInput.tsx +200 -0
  1262. package/src/react/fields/MarkdownInput.tsx +333 -0
  1263. package/src/react/fields/RadioInput.tsx +60 -0
  1264. package/src/react/fields/RepeaterInput.test.ts +116 -0
  1265. package/src/react/fields/RepeaterInput.tsx +1313 -0
  1266. package/src/react/fields/SelectFieldInput.tsx +257 -0
  1267. package/src/react/fields/SliderInput.tsx +63 -0
  1268. package/src/react/fields/TagsInput.tsx +265 -0
  1269. package/src/react/fields/TextLikeInput.tsx +54 -0
  1270. package/src/react/fields/ToggleButtonsInput.tsx +60 -0
  1271. package/src/react/fields/ToggleFieldInput.tsx +35 -0
  1272. package/src/react/fields/rowChromeButton.tsx +225 -0
  1273. package/src/react/fields/syncRowGates.test.ts +202 -0
  1274. package/src/react/fields/syncRowGates.ts +66 -0
  1275. package/src/react/formStateHelpers.test.ts +295 -0
  1276. package/src/react/formStateHelpers.ts +218 -0
  1277. package/src/react/hooks/use-mobile.ts +19 -0
  1278. package/src/react/icon-context.tsx +60 -0
  1279. package/src/react/index.ts +85 -0
  1280. package/src/react/layouts/SidebarLayout.tsx +239 -0
  1281. package/src/react/layouts/TopbarLayout.tsx +245 -0
  1282. package/src/react/navigate.tsx +37 -0
  1283. package/src/react/registry.ts +48 -0
  1284. package/src/react/right-panel-registry.tsx +47 -0
  1285. package/src/react/theme-preview/apply.ts +99 -0
  1286. package/src/react/theme-preview/build-html.ts +436 -0
  1287. package/src/react/ui/button.tsx +51 -0
  1288. package/src/react/ui/calendar.tsx +67 -0
  1289. package/src/react/ui/checkbox.tsx +29 -0
  1290. package/src/react/ui/dialog.tsx +108 -0
  1291. package/src/react/ui/dropdown-menu.tsx +97 -0
  1292. package/src/react/ui/input.tsx +20 -0
  1293. package/src/react/ui/label.tsx +21 -0
  1294. package/src/react/ui/popover.tsx +50 -0
  1295. package/src/react/ui/select.tsx +169 -0
  1296. package/src/react/ui/separator.tsx +25 -0
  1297. package/src/react/ui/sheet.tsx +136 -0
  1298. package/src/react/ui/sidebar.tsx +723 -0
  1299. package/src/react/ui/skeleton.tsx +13 -0
  1300. package/src/react/ui/slider.tsx +34 -0
  1301. package/src/react/ui/switch.tsx +28 -0
  1302. package/src/react/ui/table.tsx +105 -0
  1303. package/src/react/ui/tabs.tsx +63 -0
  1304. package/src/react/ui/textarea.tsx +18 -0
  1305. package/src/react/ui/tooltip.tsx +64 -0
  1306. package/src/react/useResizableWidth.ts +139 -0
  1307. package/src/react/utils.ts +6 -0
  1308. package/src/react/widgetRegistry.test.ts +43 -0
  1309. package/src/react/widgetRegistry.ts +50 -0
  1310. package/src/react/widgets/StatsOverviewRenderer.tsx +232 -0
  1311. package/src/react/widgets/TableWidgetRenderer.tsx +231 -0
  1312. package/src/react/widgets/ViewRenderer.tsx +71 -0
  1313. package/src/relationManagerData.test.ts +1146 -0
  1314. package/src/richtext/index.ts +8 -0
  1315. package/src/richtext/registry.ts +89 -0
  1316. package/src/routes-nested-relations.test.ts +676 -0
  1317. package/src/routes-relations.test.ts +972 -0
  1318. package/src/routes.test.ts +1886 -0
  1319. package/src/routes.ts +3262 -0
  1320. package/src/schema/Alert.test.ts +63 -0
  1321. package/src/schema/Alert.ts +49 -0
  1322. package/src/schema/Block.ts +169 -0
  1323. package/src/schema/Breadcrumbs.ts +40 -0
  1324. package/src/schema/Card.ts +35 -0
  1325. package/src/schema/Divider.ts +20 -0
  1326. package/src/schema/Element.ts +219 -0
  1327. package/src/schema/EmptyState.test.ts +37 -0
  1328. package/src/schema/EmptyState.ts +63 -0
  1329. package/src/schema/Fieldset.ts +43 -0
  1330. package/src/schema/Grid.ts +43 -0
  1331. package/src/schema/Group.ts +30 -0
  1332. package/src/schema/Heading.ts +39 -0
  1333. package/src/schema/Html.ts +67 -0
  1334. package/src/schema/Icon.ts +54 -0
  1335. package/src/schema/Image.ts +57 -0
  1336. package/src/schema/LinkTag.ts +41 -0
  1337. package/src/schema/Markdown.ts +85 -0
  1338. package/src/schema/MetaTag.ts +41 -0
  1339. package/src/schema/RelationTabs.ts +71 -0
  1340. package/src/schema/ScriptTag.ts +55 -0
  1341. package/src/schema/Section.ts +143 -0
  1342. package/src/schema/ServerDataElement.test.ts +140 -0
  1343. package/src/schema/ServerDataElement.ts +156 -0
  1344. package/src/schema/Split.ts +50 -0
  1345. package/src/schema/Stat.test.ts +118 -0
  1346. package/src/schema/Stat.ts +154 -0
  1347. package/src/schema/StatsOverview.test.ts +141 -0
  1348. package/src/schema/StatsOverview.ts +119 -0
  1349. package/src/schema/StyleTag.ts +35 -0
  1350. package/src/schema/TableWidget.test.ts +297 -0
  1351. package/src/schema/TableWidget.ts +289 -0
  1352. package/src/schema/Tabs.ts +79 -0
  1353. package/src/schema/Text.ts +58 -0
  1354. package/src/schema/UnorderedList.ts +49 -0
  1355. package/src/schema/View.test.ts +111 -0
  1356. package/src/schema/View.ts +127 -0
  1357. package/src/schema/Wizard.ts +108 -0
  1358. package/src/schema/containers.test.ts +446 -0
  1359. package/src/schema/headTags.test.ts +134 -0
  1360. package/src/schema/index.ts +39 -0
  1361. package/src/schema/primes.test.ts +269 -0
  1362. package/src/schema/resolveSchema.test.ts +329 -0
  1363. package/src/schema/resolveSchema.ts +807 -0
  1364. package/src/schema/sanitize.ts +49 -0
  1365. package/src/search.test.ts +446 -0
  1366. package/src/search.ts +178 -0
  1367. package/src/sessionFilters.test.ts +352 -0
  1368. package/src/sessionFilters.ts +133 -0
  1369. package/src/summarizers/Summarizer.test.ts +84 -0
  1370. package/src/summarizers/Summarizer.ts +123 -0
  1371. package/src/summarizers/index.ts +11 -0
  1372. package/src/theme/base-colors.ts +68 -0
  1373. package/src/theme/chart-colors.ts +50 -0
  1374. package/src/theme/colors.ts +447 -0
  1375. package/src/theme/generate-css.test.ts +139 -0
  1376. package/src/theme/generate-css.ts +44 -0
  1377. package/src/theme/generate-scale.test.ts +106 -0
  1378. package/src/theme/generate-scale.ts +97 -0
  1379. package/src/theme/icon-map.ts +42 -0
  1380. package/src/theme/index.ts +28 -0
  1381. package/src/theme/migrate.ts +81 -0
  1382. package/src/theme/presets.ts +135 -0
  1383. package/src/theme/radius.ts +18 -0
  1384. package/src/theme/resolve.test.ts +238 -0
  1385. package/src/theme/resolve.ts +96 -0
  1386. package/src/theme/spacing.ts +18 -0
  1387. package/src/theme/theme-colors.ts +88 -0
  1388. package/src/theme/types.ts +125 -0
  1389. package/src/uploads/UploadAdapter.ts +35 -0
  1390. package/src/uploads/index.ts +2 -0
  1391. package/src/uploads/localUpload.test.ts +70 -0
  1392. package/src/uploads/localUpload.ts +84 -0
  1393. package/src/validation/Validator.ts +49 -0
  1394. package/src/validation/index.ts +28 -0
  1395. package/src/validation/rules.ts +78 -0
  1396. package/src/validation/runValidators.ts +435 -0
  1397. package/src/validation/uniqueValidator.test.ts +196 -0
  1398. package/src/validation/uniqueValidator.ts +133 -0
  1399. package/src/validation/validators.test.ts +268 -0
  1400. package/src/vite.ts +758 -0
  1401. package/src/widgets/index.ts +10 -0
  1402. package/src/widgets/registry.ts +45 -0
  1403. package/src/widgets.test.ts +592 -0
  1404. package/tsconfig.build.json +11 -0
  1405. package/tsconfig.json +4 -0
  1406. package/tsconfig.test.json +10 -0
  1407. package/views/react/Dashboard.tsx +27 -0
  1408. package/views/react/Resources/Form.tsx +102 -0
  1409. package/views/react/Resources/Index.tsx +49 -0
@@ -0,0 +1,2479 @@
1
+ import { describe, it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { Action } from './Action.js'
5
+ import { resolveSchema, _resetResolverRegistry } from '../schema/resolveSchema.js'
6
+ import { Card } from '../schema/Card.js'
7
+ import { RelationManager, type RelationManagerContext } from '../RelationManager.js'
8
+
9
+ beforeEach(() => _resetResolverRegistry())
10
+
11
+ describe('Action.toMeta', () => {
12
+ it('emits required fields with sensible defaults', () => {
13
+ const meta = Action.make('publish').toMeta()
14
+ assert.equal(meta.type, 'action')
15
+ assert.equal(meta.name, 'publish')
16
+ assert.equal(meta.label, 'Publish') // auto-derived from name
17
+ assert.equal(meta.placement, 'inline')
18
+ assert.equal(meta.destructive, false)
19
+ assert.equal(meta.icon, undefined)
20
+ assert.equal(meta.confirm, undefined)
21
+ })
22
+
23
+ it('label() overrides the auto-derived label', () => {
24
+ const meta = Action.make('publish').label('Publish Now').toMeta()
25
+ assert.equal(meta.label, 'Publish Now')
26
+ })
27
+
28
+ it('icon() emits the icon string', () => {
29
+ const meta = Action.make('save').icon('check').toMeta()
30
+ assert.equal(meta.icon, 'check')
31
+ })
32
+
33
+ it('destructive() flips the flag', () => {
34
+ const meta = Action.make('delete').destructive().toMeta()
35
+ assert.equal(meta.destructive, true)
36
+ })
37
+
38
+ describe('placement', () => {
39
+ it('placement(p) sets it directly', () => {
40
+ assert.equal(Action.make('a').placement('row').toMeta().placement, 'row')
41
+ assert.equal(Action.make('a').placement('bulk').toMeta().placement, 'bulk')
42
+ assert.equal(Action.make('a').placement('header').toMeta().placement, 'header')
43
+ })
44
+
45
+ it('shorthand setters .row() / .bulk() / .header() / .inline()', () => {
46
+ assert.equal(Action.make('a').row().toMeta().placement, 'row')
47
+ assert.equal(Action.make('a').bulk().toMeta().placement, 'bulk')
48
+ assert.equal(Action.make('a').header().toMeta().placement, 'header')
49
+ assert.equal(Action.make('a').row().inline().toMeta().placement, 'inline')
50
+ })
51
+ })
52
+
53
+ describe('confirm', () => {
54
+ it('string shorthand becomes { message }', () => {
55
+ const meta = Action.make('delete').confirm('Are you sure?').toMeta()
56
+ assert.deepEqual(meta.confirm, { message: 'Are you sure?' })
57
+ })
58
+
59
+ it('object form preserves all keys', () => {
60
+ const meta = Action.make('delete').confirm({
61
+ title: 'Delete user',
62
+ message: 'This action cannot be undone.',
63
+ confirmLabel: 'Yes, delete',
64
+ }).toMeta()
65
+ assert.deepEqual(meta.confirm, {
66
+ title: 'Delete user',
67
+ message: 'This action cannot be undone.',
68
+ confirmLabel: 'Yes, delete',
69
+ })
70
+ })
71
+
72
+ it('omitted when not set', () => {
73
+ assert.equal('confirm' in Action.make('save').toMeta(), false)
74
+ })
75
+ })
76
+
77
+ describe('handler', () => {
78
+ it('is stored but does not appear in serialized meta', () => {
79
+ const fn = async () => {}
80
+ const a = Action.make('publish').handler(fn)
81
+ assert.equal(a.getHandler(), fn)
82
+ assert.equal('handler' in a.toMeta(), false)
83
+ })
84
+ })
85
+ })
86
+
87
+ describe('Action in the schema tree', () => {
88
+ it('resolves with type=action via the unified resolver', async () => {
89
+ const result = await resolveSchema([Action.make('save').icon('check')])
90
+ assert.equal(result[0]!.type, 'action')
91
+ assert.equal(result[0]!['name'], 'save')
92
+ assert.equal(result[0]!['icon'], 'check')
93
+ })
94
+
95
+ it('appears as a child inside a container Element', async () => {
96
+ const tree = [
97
+ Card.make('Header').schema([
98
+ Action.make('export').header(),
99
+ Action.make('delete').row().destructive().confirm('Sure?'),
100
+ ]),
101
+ ]
102
+ const result = await resolveSchema(tree)
103
+ assert.equal(result[0]!.children?.length, 2)
104
+ assert.equal(result[0]!.children![0]!.type, 'action')
105
+ assert.equal(result[0]!.children![0]!['placement'], 'header')
106
+ assert.equal(result[0]!.children![1]!['placement'], 'row')
107
+ assert.deepEqual(result[0]!.children![1]!['confirm'], { message: 'Sure?' })
108
+ })
109
+ })
110
+
111
+ describe('Action variants & cosmetics', () => {
112
+ it('color() sets the visual color', () => {
113
+ assert.equal(Action.make('a').color('success').toMeta().color, 'success')
114
+ assert.equal(Action.make('a').color('warning').toMeta().color, 'warning')
115
+ assert.equal(Action.make('a').color('ghost').toMeta().color, 'ghost')
116
+ })
117
+
118
+ it('destructive() implies color="destructive" when no explicit color', () => {
119
+ const meta = Action.make('delete').destructive().toMeta()
120
+ assert.equal(meta.destructive, true)
121
+ assert.equal(meta.color, 'destructive')
122
+ })
123
+
124
+ it('explicit color() wins over destructive() flag', () => {
125
+ const meta = Action.make('warn').destructive().color('warning').toMeta()
126
+ assert.equal(meta.destructive, true)
127
+ assert.equal(meta.color, 'warning')
128
+ })
129
+
130
+ it('size() sets the size preset', () => {
131
+ assert.equal(Action.make('a').size('sm').toMeta().size, 'sm')
132
+ assert.equal(Action.make('a').size('lg').toMeta().size, 'lg')
133
+ })
134
+
135
+ it('tooltip() round-trips', () => {
136
+ const meta = Action.make('save').tooltip('Save changes').toMeta()
137
+ assert.equal(meta.tooltip, 'Save changes')
138
+ })
139
+
140
+ it('outlined() emits the flag only when set', () => {
141
+ assert.equal(Action.make('a').toMeta().outlined, undefined)
142
+ assert.equal(Action.make('a').outlined().toMeta().outlined, true)
143
+ })
144
+
145
+ it('iconButton() emits iconOnly: true', () => {
146
+ const meta = Action.make('refresh').icon('refresh').iconButton().toMeta()
147
+ assert.equal(meta.iconOnly, true)
148
+ })
149
+
150
+ it('badge() / badgeColor() round-trip', () => {
151
+ const meta = Action.make('inbox').badge(7).badgeColor('bg-red-500').toMeta()
152
+ assert.equal(meta.badge, 7)
153
+ assert.equal(meta.badgeColor, 'bg-red-500')
154
+ })
155
+
156
+ it('cosmetic builders are absent from meta when not called', () => {
157
+ const meta = Action.make('plain').toMeta()
158
+ assert.equal(meta.color, undefined)
159
+ assert.equal(meta.size, undefined)
160
+ assert.equal(meta.tooltip, undefined)
161
+ assert.equal(meta.outlined, undefined)
162
+ assert.equal(meta.iconOnly, undefined)
163
+ assert.equal(meta.badge, undefined)
164
+ })
165
+ })
166
+
167
+ describe('Action visibility evaluation', () => {
168
+ it('default — no rules → visible:true, disabled:false', async () => {
169
+ const a = Action.make('a')
170
+ assert.deepEqual(await a.evaluate(), { visible: true, disabled: false })
171
+ assert.equal(a.hasVisibilityRules(), false)
172
+ })
173
+
174
+ it('visible(false) hides the action', async () => {
175
+ assert.equal((await Action.make('a').visible(false).evaluate()).visible, false)
176
+ })
177
+
178
+ it('hidden(true) hides the action', async () => {
179
+ assert.equal((await Action.make('a').hidden(true).evaluate()).visible, false)
180
+ })
181
+
182
+ it('visible(fn) receives the context', async () => {
183
+ const a = Action.make('a').visible(({ record }) => Boolean((record as { active?: boolean })?.active))
184
+ assert.equal((await a.evaluate({ record: { active: true } })).visible, true)
185
+ assert.equal((await a.evaluate({ record: { active: false } })).visible, false)
186
+ assert.equal((await a.evaluate({ record: undefined })).visible, false)
187
+ })
188
+
189
+ it('disabled(fn) receives the context', async () => {
190
+ const a = Action.make('a').disabled(({ record }) => Boolean((record as { locked?: boolean })?.locked))
191
+ assert.equal((await a.evaluate({ record: { locked: true } })).disabled, true)
192
+ assert.equal((await a.evaluate({ record: { locked: false } })).disabled, false)
193
+ })
194
+
195
+ it('combines visible and hidden via AND (visible && !hidden)', async () => {
196
+ const a = Action.make('a').visible(true).hidden(({ record }) => (record as { trashed?: boolean })?.trashed === true)
197
+ assert.equal((await a.evaluate({ record: { trashed: false } })).visible, true)
198
+ assert.equal((await a.evaluate({ record: { trashed: true } })).visible, false)
199
+ })
200
+
201
+ it('authorize() is an alias for visible()', async () => {
202
+ const a = Action.make('a').authorize(({ user }) => Boolean((user as { admin?: boolean })?.admin))
203
+ assert.equal((await a.evaluate({ user: { admin: true } })).visible, true)
204
+ assert.equal((await a.evaluate({ user: { admin: false } })).visible, false)
205
+ })
206
+
207
+ it('async visibility rule resolves a Promise<boolean>', async () => {
208
+ const a = Action.make('a').visible(async ({ user }) => Boolean((user as { admin?: boolean })?.admin))
209
+ assert.equal((await a.evaluate({ user: { admin: true } })).visible, true)
210
+ assert.equal((await a.evaluate({ user: { admin: false } })).visible, false)
211
+ })
212
+
213
+ it('throwing visibility rule fails closed (not visible)', async () => {
214
+ const a = Action.make('a').visible(() => { throw new Error('boom') })
215
+ assert.equal((await a.evaluate()).visible, false)
216
+ })
217
+
218
+ it('hasVisibilityRules returns true when any rule is set', () => {
219
+ assert.equal(Action.make('a').visible(true).hasVisibilityRules(), true)
220
+ assert.equal(Action.make('a').hidden(false).hasVisibilityRules(), true)
221
+ assert.equal(Action.make('a').disabled(false).hasVisibilityRules(), true)
222
+ assert.equal(Action.make('a').authorize(true).hasVisibilityRules(), true)
223
+ })
224
+
225
+ it('toMeta emits conditional:true when rules exist', () => {
226
+ assert.equal(Action.make('a').toMeta().conditional, undefined)
227
+ assert.equal(Action.make('a').visible(true).toMeta().conditional, true)
228
+ })
229
+ })
230
+
231
+ describe('Action.relation* factories (Plan #11 polish)', () => {
232
+ /** Bare manager + ctx pair shared across the tests below. */
233
+ class Posts extends RelationManager {
234
+ static override relationship = 'posts'
235
+ static override label = 'Posts'
236
+ static override labelSingular = 'Post'
237
+ }
238
+
239
+ const ctx: RelationManagerContext = {
240
+ basePath: '/admin',
241
+ parentSlug: 'users',
242
+ parentId: '42',
243
+ relationship: 'posts',
244
+ parentRecord: { id: '42' },
245
+ mode: 'hasMany',
246
+ }
247
+
248
+ describe('relationCreate', () => {
249
+ it('builds the create URL under the parent record', () => {
250
+ const meta = Action.relationCreate(Posts, ctx).toMeta()
251
+ assert.equal(meta.href, '/admin/users/42/posts/create')
252
+ assert.equal(meta.label, 'New Post')
253
+ assert.equal(meta.method, undefined) // link-style, not form-post
254
+ })
255
+
256
+ it('label uses the manager singular fallback when not pinned', () => {
257
+ class Comments extends RelationManager { static override relationship = 'comments' }
258
+ const meta = Action.relationCreate(Comments, { ...ctx, relationship: 'comments' }).toMeta()
259
+ assert.equal(meta.label, 'New Comment')
260
+ })
261
+
262
+ it('visibility delegates to manager.canCreate when overridden', async () => {
263
+ class Forbidden extends RelationManager {
264
+ static override relationship = 'posts'
265
+ static override async canCreate(): Promise<boolean> { return false }
266
+ }
267
+ const result = await Action.relationCreate(Forbidden, ctx).evaluate({})
268
+ assert.equal(result.visible, false)
269
+ })
270
+
271
+ it('falls through to related Resource canCreate when manager unset', async () => {
272
+ const Related = { canCreate: async () => false } as unknown as RelationManagerContext['related']
273
+ const result = await Action.relationCreate(Posts, { ...ctx, related: Related }).evaluate({})
274
+ assert.equal(result.visible, false)
275
+ })
276
+
277
+ it('allows when neither manager nor related Resource opts in', async () => {
278
+ const result = await Action.relationCreate(Posts, ctx).evaluate({})
279
+ assert.equal(result.visible, true)
280
+ })
281
+
282
+ it('auto-hides under belongsToMany mode (no per-pivot create)', async () => {
283
+ const m2mCtx = { ...ctx, mode: 'belongsToMany' as const }
284
+ const result = await Action.relationCreate(Posts, m2mCtx).evaluate({})
285
+ assert.equal(result.visible, false)
286
+ })
287
+ })
288
+
289
+ describe('relationEdit', () => {
290
+ it('builds the edit URL with :id template for row context', () => {
291
+ const meta = Action.relationEdit(Posts, ctx).toMeta()
292
+ assert.equal(meta.href, '/admin/users/42/posts/:id/edit')
293
+ assert.equal(meta.label, 'Edit')
294
+ })
295
+
296
+ it('bakes in an explicit recordId when provided', () => {
297
+ const meta = Action.relationEdit(Posts, ctx, '7').toMeta()
298
+ assert.equal(meta.href, '/admin/users/42/posts/7/edit')
299
+ })
300
+
301
+ it('visibility receives both the row record and the parentRecord via ctx', async () => {
302
+ let seenChild: unknown
303
+ let seenParent: unknown
304
+ class WithEdit extends RelationManager {
305
+ static override relationship = 'posts'
306
+ static override async canEdit(_user: unknown, child: unknown, parent: unknown): Promise<boolean> {
307
+ seenChild = child
308
+ seenParent = parent
309
+ return true
310
+ }
311
+ }
312
+ const a = Action.relationEdit(WithEdit, ctx)
313
+ await a.evaluate({ record: { id: '7', title: 'A' } })
314
+ assert.deepEqual(seenChild, { id: '7', title: 'A' })
315
+ assert.deepEqual(seenParent, { id: '42' })
316
+ })
317
+
318
+ it('auto-hides under belongsToMany mode (no per-pivot edit)', async () => {
319
+ const m2mCtx = { ...ctx, mode: 'belongsToMany' as const }
320
+ const result = await Action.relationEdit(Posts, m2mCtx).evaluate({ record: { id: '9' } })
321
+ assert.equal(result.visible, false)
322
+ })
323
+ })
324
+
325
+ describe('relationDelete', () => {
326
+ it('builds a destructive POST to the delete URL with confirm prompt', () => {
327
+ const meta = Action.relationDelete(Posts, ctx).toMeta()
328
+ assert.equal(meta.method, 'post')
329
+ assert.equal(meta.action, '/admin/users/42/posts/:id/delete')
330
+ assert.equal(meta.destructive, true)
331
+ assert.match(meta.confirm?.message ?? '', /post/)
332
+ })
333
+
334
+ it('honors an explicit recordId at config time', () => {
335
+ const meta = Action.relationDelete(Posts, ctx, '7').toMeta()
336
+ assert.equal(meta.action, '/admin/users/42/posts/7/delete')
337
+ })
338
+
339
+ it('visibility absorbs predicate throws as false (fail-closed)', async () => {
340
+ class Throwing extends RelationManager {
341
+ static override relationship = 'posts'
342
+ static override async canDelete(): Promise<boolean> { throw new Error('boom') }
343
+ }
344
+ const result = await Action.relationDelete(Throwing, ctx).evaluate({ record: { id: '7' } })
345
+ assert.equal(result.visible, false)
346
+ })
347
+
348
+ it('hides on already-trashed rows when related Resource has softDeletes=true', async () => {
349
+ const Related = { softDeletes: true } as unknown as RelationManagerContext['related']
350
+ const a = Action.relationDelete(Posts, { ...ctx, related: Related })
351
+ assert.equal((await a.evaluate({ record: { id: '7', deletedAt: '2026-01-01' } })).visible, false)
352
+ })
353
+
354
+ it('still shows on live rows when related Resource has softDeletes=true', async () => {
355
+ const Related = { softDeletes: true } as unknown as RelationManagerContext['related']
356
+ const a = Action.relationDelete(Posts, { ...ctx, related: Related })
357
+ assert.equal((await a.evaluate({ record: { id: '7' } })).visible, true)
358
+ })
359
+
360
+ it('honors a custom deletedAtColumn from the related Resource', async () => {
361
+ const Related = { softDeletes: true, deletedAtColumn: 'archivedAt' } as unknown as RelationManagerContext['related']
362
+ const a = Action.relationDelete(Posts, { ...ctx, related: Related })
363
+ assert.equal((await a.evaluate({ record: { archivedAt: '2026-01-01' } })).visible, false)
364
+ assert.equal((await a.evaluate({ record: { archivedAt: null } })).visible, true)
365
+ })
366
+
367
+ it('auto-hides under belongsToMany mode (use relationDetach instead)', async () => {
368
+ const m2mCtx = { ...ctx, mode: 'belongsToMany' as const }
369
+ const result = await Action.relationDelete(Posts, m2mCtx).evaluate({ record: { id: '9' } })
370
+ assert.equal(result.visible, false)
371
+ })
372
+ })
373
+
374
+ // ── Plan #13 polish — relationRestore / relationForceDelete ────
375
+
376
+ describe('relationRestore', () => {
377
+ const Related = { softDeletes: true } as unknown as RelationManagerContext['related']
378
+ const softCtx: RelationManagerContext = { ...ctx, related: Related }
379
+
380
+ it('builds the restore URL under the parent record with success color', () => {
381
+ const meta = Action.relationRestore(Posts, softCtx).toMeta()
382
+ assert.equal(meta.method, 'post')
383
+ assert.equal(meta.action, '/admin/users/42/posts/:id/restore')
384
+ assert.equal(meta.label, 'Restore')
385
+ assert.equal(meta.color, 'success')
386
+ })
387
+
388
+ it('honors an explicit recordId at config time', () => {
389
+ const meta = Action.relationRestore(Posts, softCtx, '7').toMeta()
390
+ assert.equal(meta.action, '/admin/users/42/posts/7/restore')
391
+ })
392
+
393
+ it('hides on live (non-trashed) rows', async () => {
394
+ const a = Action.relationRestore(Posts, softCtx)
395
+ assert.equal((await a.evaluate({ record: { id: '7' } })).visible, false)
396
+ })
397
+
398
+ it('shows on trashed rows by default (manager default canRestore = true)', async () => {
399
+ const a = Action.relationRestore(Posts, softCtx)
400
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, true)
401
+ })
402
+
403
+ it('hides entirely when the related Resource does not opt into softDeletes', async () => {
404
+ const NonSoft = { softDeletes: false } as unknown as RelationManagerContext['related']
405
+ const a = Action.relationRestore(Posts, { ...ctx, related: NonSoft })
406
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, false)
407
+ })
408
+
409
+ it('respects the manager canRestore override', async () => {
410
+ class Locked extends RelationManager {
411
+ static override relationship = 'posts'
412
+ static override async canRestore(): Promise<boolean> { return false }
413
+ }
414
+ const a = Action.relationRestore(Locked, softCtx)
415
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, false)
416
+ })
417
+
418
+ it('falls through to related Resource canRestore when manager unset', async () => {
419
+ const RelatedDeny = {
420
+ softDeletes: true,
421
+ canRestore: async () => false,
422
+ } as unknown as RelationManagerContext['related']
423
+ const a = Action.relationRestore(Posts, { ...ctx, related: RelatedDeny })
424
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, false)
425
+ })
426
+ })
427
+
428
+ describe('relationForceDelete', () => {
429
+ const Related = { softDeletes: true } as unknown as RelationManagerContext['related']
430
+ const softCtx: RelationManagerContext = { ...ctx, related: Related }
431
+
432
+ it('builds a destructive POST to the force-delete URL with permanence confirm', () => {
433
+ const meta = Action.relationForceDelete(Posts, softCtx).toMeta()
434
+ assert.equal(meta.method, 'post')
435
+ assert.equal(meta.action, '/admin/users/42/posts/:id/force-delete')
436
+ assert.equal(meta.label, 'Delete forever')
437
+ assert.equal(meta.destructive, true)
438
+ assert.match(meta.confirm?.message ?? '', /cannot be undone/i)
439
+ })
440
+
441
+ it('honors an explicit recordId at config time', () => {
442
+ const meta = Action.relationForceDelete(Posts, softCtx, '7').toMeta()
443
+ assert.equal(meta.action, '/admin/users/42/posts/7/force-delete')
444
+ })
445
+
446
+ it('hides on live (non-trashed) rows', async () => {
447
+ const a = Action.relationForceDelete(Posts, softCtx)
448
+ assert.equal((await a.evaluate({ record: { id: '7' } })).visible, false)
449
+ })
450
+
451
+ it('shows on trashed rows by default (canForceDelete inherits canDelete = true)', async () => {
452
+ const a = Action.relationForceDelete(Posts, softCtx)
453
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, true)
454
+ })
455
+
456
+ it('hides when the related Resource does not opt into softDeletes', async () => {
457
+ const NonSoft = { softDeletes: false } as unknown as RelationManagerContext['related']
458
+ const a = Action.relationForceDelete(Posts, { ...ctx, related: NonSoft })
459
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, false)
460
+ })
461
+
462
+ it('inherits canDelete denial when canForceDelete is not overridden', async () => {
463
+ class Locked extends RelationManager {
464
+ static override relationship = 'posts'
465
+ static override async canDelete(): Promise<boolean> { return false }
466
+ // canForceDelete inherits its default which delegates to canDelete
467
+ }
468
+ const a = Action.relationForceDelete(Locked, softCtx)
469
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, false)
470
+ })
471
+
472
+ it('respects an explicit canForceDelete override stricter than canDelete', async () => {
473
+ class Stricter extends RelationManager {
474
+ static override relationship = 'posts'
475
+ // canDelete defaults to true (inherited)
476
+ static override async canForceDelete(): Promise<boolean> { return false }
477
+ }
478
+ const a = Action.relationForceDelete(Stricter, softCtx)
479
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, false)
480
+ })
481
+ })
482
+
483
+ describe('M2M factories — relationAttach', () => {
484
+ class Tags extends RelationManager {
485
+ static override relationship = 'tags'
486
+ static override label = 'Tags'
487
+ static override labelSingular = 'Tag'
488
+ }
489
+
490
+ const m2mCtx: RelationManagerContext = {
491
+ basePath: '/admin',
492
+ parentSlug: 'articles',
493
+ parentId: '5',
494
+ relationship: 'tags',
495
+ parentRecord: { id: '5' },
496
+ mode: 'belongsToMany',
497
+ }
498
+
499
+ it('renders as a header action with an Attach label', () => {
500
+ const meta = Action.relationAttach(Tags, m2mCtx).toMeta()
501
+ assert.equal(meta.label, 'Attach Tag')
502
+ assert.equal(meta.placement, 'header')
503
+ })
504
+
505
+ it('builds a modal-form schema only when M2M + Related.model are set', () => {
506
+ // No related Resource → schema stays empty (action would still
507
+ // mount but the modal has nothing to pick from). `.getSchema()`
508
+ // returns the raw Element[] before resolver-side serialization;
509
+ // `toMeta().children` is populated by the schema walker, not by
510
+ // the action itself, so we check the unresolved tree directly.
511
+ const noRelated = Action.relationAttach(Tags, m2mCtx)
512
+ assert.equal(noRelated.getSchema().length, 0)
513
+
514
+ // With a stub related model present, the schema gains a SelectField.
515
+ const Related = {
516
+ model: { query: () => ({ paginate: async () => ({ data: [], total: 0 }) }) },
517
+ } as unknown as RelationManagerContext['related']
518
+ const withRelated = Action.relationAttach(Tags, { ...m2mCtx, related: Related })
519
+ assert.equal(withRelated.getSchema().length, 1)
520
+ })
521
+
522
+ it('auto-hides under hasMany mode (drop-in safety)', async () => {
523
+ const hasManyCtx = { ...m2mCtx, mode: 'hasMany' as const }
524
+ const result = await Action.relationAttach(Tags, hasManyCtx).evaluate({})
525
+ assert.equal(result.visible, false)
526
+ })
527
+
528
+ it('visible when canAttach defaults true under M2M mode', async () => {
529
+ const result = await Action.relationAttach(Tags, m2mCtx).evaluate({})
530
+ assert.equal(result.visible, true)
531
+ })
532
+
533
+ it('manager canAttach=false hides the action even under M2M', async () => {
534
+ class Locked extends RelationManager {
535
+ static override relationship = 'tags'
536
+ static override async canAttach(): Promise<boolean> { return false }
537
+ }
538
+ const result = await Action.relationAttach(Locked, m2mCtx).evaluate({})
539
+ assert.equal(result.visible, false)
540
+ })
541
+ })
542
+
543
+ describe('M2M factories — relationDetach', () => {
544
+ class Tags extends RelationManager {
545
+ static override relationship = 'tags'
546
+ static override label = 'Tags'
547
+ static override labelSingular = 'Tag'
548
+ }
549
+
550
+ const m2mCtx: RelationManagerContext = {
551
+ basePath: '/admin',
552
+ parentSlug: 'articles',
553
+ parentId: '5',
554
+ relationship: 'tags',
555
+ parentRecord: { id: '5' },
556
+ mode: 'belongsToMany',
557
+ }
558
+
559
+ it('builds the detach URL with :id template for row context', () => {
560
+ const meta = Action.relationDetach(Tags, m2mCtx).toMeta()
561
+ assert.equal(meta.action, '/admin/articles/5/tags/:id/_detach')
562
+ assert.equal(meta.method, 'post')
563
+ assert.equal(meta.label, 'Detach')
564
+ })
565
+
566
+ it('bakes in an explicit recordId when provided', () => {
567
+ const meta = Action.relationDetach(Tags, m2mCtx, '9').toMeta()
568
+ assert.equal(meta.action, '/admin/articles/5/tags/9/_detach')
569
+ })
570
+
571
+ it('auto-hides under hasMany mode', async () => {
572
+ const hasManyCtx = { ...m2mCtx, mode: 'hasMany' as const }
573
+ const result = await Action.relationDetach(Tags, hasManyCtx).evaluate({ record: { id: 9 } })
574
+ assert.equal(result.visible, false)
575
+ })
576
+
577
+ it('visible when canDetach defaults true under M2M mode', async () => {
578
+ const result = await Action.relationDetach(Tags, m2mCtx).evaluate({ record: { id: 9 } })
579
+ assert.equal(result.visible, true)
580
+ })
581
+
582
+ it('confirm message frames the operation as detach (not delete)', () => {
583
+ const meta = Action.relationDetach(Tags, m2mCtx).toMeta() as { confirm?: { message: string } }
584
+ assert.match(meta.confirm?.message ?? '', /Detach/)
585
+ assert.match(meta.confirm?.message ?? '', /stays in place/)
586
+ })
587
+ })
588
+
589
+ describe('M2M factories — relationBulkDetach', () => {
590
+ class Tags extends RelationManager {
591
+ static override relationship = 'tags'
592
+ static override label = 'Tags'
593
+ static override labelSingular = 'Tag'
594
+ }
595
+
596
+ const m2mCtx: RelationManagerContext = {
597
+ basePath: '/admin',
598
+ parentSlug: 'articles',
599
+ parentId: '5',
600
+ relationship: 'tags',
601
+ parentRecord: { id: '5' },
602
+ mode: 'belongsToMany',
603
+ }
604
+
605
+ it('renders as a bulk action with destructive styling', () => {
606
+ const meta = Action.relationBulkDetach(Tags, m2mCtx).toMeta()
607
+ assert.equal(meta.placement, 'bulk')
608
+ assert.equal(meta.color, 'destructive')
609
+ })
610
+
611
+ it('auto-hides under hasMany mode', async () => {
612
+ const hasManyCtx = { ...m2mCtx, mode: 'hasMany' as const }
613
+ const result = await Action.relationBulkDetach(Tags, hasManyCtx).evaluate({})
614
+ assert.equal(result.visible, false)
615
+ })
616
+
617
+ it('visible when canAttach defaults true under M2M mode (bulk uses canAttach as gate)', async () => {
618
+ const result = await Action.relationBulkDetach(Tags, m2mCtx).evaluate({})
619
+ assert.equal(result.visible, true)
620
+ })
621
+ })
622
+
623
+ // ─── Polymorphic M2M follow-up — morphToMany / morphedByMany ──────
624
+ //
625
+ // morphToMany (owning polymorphic side, e.g. `Post.tags()`) and
626
+ // morphedByMany (inverse polymorphic side, e.g. `Tag.posts()`) share
627
+ // the `belongsToMany` pivot-mutation shape. The three M2M factories
628
+ // (`relationAttach / Detach / BulkDetach`) and the three "no per-pivot
629
+ // surface" auto-hides on `relationCreate / Edit / Delete` should treat
630
+ // all three modes identically.
631
+
632
+ describe('M2M factories — morphToMany (owning polymorphic side)', () => {
633
+ class Tags extends RelationManager {
634
+ static override relationship = 'tags'
635
+ static override label = 'Tags'
636
+ static override labelSingular = 'Tag'
637
+ }
638
+
639
+ const morphToManyCtx: RelationManagerContext = {
640
+ basePath: '/admin',
641
+ parentSlug: 'posts',
642
+ parentId: '5',
643
+ relationship: 'tags',
644
+ parentRecord: { id: '5' },
645
+ mode: 'morphToMany',
646
+ }
647
+
648
+ it('relationAttach renders as header action under morphToMany mode', () => {
649
+ const meta = Action.relationAttach(Tags, morphToManyCtx).toMeta()
650
+ assert.equal(meta.label, 'Attach Tag')
651
+ assert.equal(meta.placement, 'header')
652
+ })
653
+
654
+ it('relationAttach builds modal-form schema under morphToMany when Related.model is set', () => {
655
+ const Related = {
656
+ model: { query: () => ({ paginate: async () => ({ data: [], total: 0 }) }) },
657
+ } as unknown as RelationManagerContext['related']
658
+ const a = Action.relationAttach(Tags, { ...morphToManyCtx, related: Related })
659
+ assert.equal(a.getSchema().length, 1)
660
+ })
661
+
662
+ it('relationAttach visible when canAttach defaults true under morphToMany mode', async () => {
663
+ const result = await Action.relationAttach(Tags, morphToManyCtx).evaluate({})
664
+ assert.equal(result.visible, true)
665
+ })
666
+
667
+ it('relationDetach builds detach URL under morphToMany mode', () => {
668
+ const meta = Action.relationDetach(Tags, morphToManyCtx).toMeta()
669
+ assert.equal(meta.action, '/admin/posts/5/tags/:id/_detach')
670
+ assert.equal(meta.method, 'post')
671
+ })
672
+
673
+ it('relationDetach visible when canDetach defaults true under morphToMany mode', async () => {
674
+ const result = await Action.relationDetach(Tags, morphToManyCtx).evaluate({ record: { id: 9 } })
675
+ assert.equal(result.visible, true)
676
+ })
677
+
678
+ it('relationBulkDetach visible under morphToMany mode', async () => {
679
+ const result = await Action.relationBulkDetach(Tags, morphToManyCtx).evaluate({})
680
+ assert.equal(result.visible, true)
681
+ })
682
+
683
+ it('relationCreate auto-hides under morphToMany (no per-pivot create surface)', async () => {
684
+ const result = await Action.relationCreate(Tags, morphToManyCtx).evaluate({})
685
+ assert.equal(result.visible, false)
686
+ })
687
+
688
+ it('relationEdit auto-hides under morphToMany (no per-pivot edit surface)', async () => {
689
+ const result = await Action.relationEdit(Tags, morphToManyCtx).evaluate({ record: { id: '9' } })
690
+ assert.equal(result.visible, false)
691
+ })
692
+
693
+ it('relationDelete auto-hides under morphToMany (use relationDetach instead)', async () => {
694
+ const result = await Action.relationDelete(Tags, morphToManyCtx).evaluate({ record: { id: '9' } })
695
+ assert.equal(result.visible, false)
696
+ })
697
+ })
698
+
699
+ describe('M2M factories — morphedByMany (inverse polymorphic side)', () => {
700
+ class Posts extends RelationManager {
701
+ static override relationship = 'posts'
702
+ static override label = 'Posts'
703
+ static override labelSingular = 'Post'
704
+ }
705
+
706
+ const morphedByManyCtx: RelationManagerContext = {
707
+ basePath: '/admin',
708
+ parentSlug: 'tags',
709
+ parentId: '5',
710
+ relationship: 'posts',
711
+ parentRecord: { id: '5' },
712
+ mode: 'morphedByMany',
713
+ }
714
+
715
+ it('relationAttach renders as header action under morphedByMany mode', () => {
716
+ const meta = Action.relationAttach(Posts, morphedByManyCtx).toMeta()
717
+ assert.equal(meta.label, 'Attach Post')
718
+ assert.equal(meta.placement, 'header')
719
+ })
720
+
721
+ it('relationAttach builds modal-form schema under morphedByMany when Related.model is set', () => {
722
+ const Related = {
723
+ model: { query: () => ({ paginate: async () => ({ data: [], total: 0 }) }) },
724
+ } as unknown as RelationManagerContext['related']
725
+ const a = Action.relationAttach(Posts, { ...morphedByManyCtx, related: Related })
726
+ assert.equal(a.getSchema().length, 1)
727
+ })
728
+
729
+ it('relationAttach visible when canAttach defaults true under morphedByMany mode', async () => {
730
+ const result = await Action.relationAttach(Posts, morphedByManyCtx).evaluate({})
731
+ assert.equal(result.visible, true)
732
+ })
733
+
734
+ it('relationDetach builds detach URL under morphedByMany mode', () => {
735
+ const meta = Action.relationDetach(Posts, morphedByManyCtx).toMeta()
736
+ assert.equal(meta.action, '/admin/tags/5/posts/:id/_detach')
737
+ assert.equal(meta.method, 'post')
738
+ })
739
+
740
+ it('relationDetach visible when canDetach defaults true under morphedByMany mode', async () => {
741
+ const result = await Action.relationDetach(Posts, morphedByManyCtx).evaluate({ record: { id: 9 } })
742
+ assert.equal(result.visible, true)
743
+ })
744
+
745
+ it('relationBulkDetach visible under morphedByMany mode', async () => {
746
+ const result = await Action.relationBulkDetach(Posts, morphedByManyCtx).evaluate({})
747
+ assert.equal(result.visible, true)
748
+ })
749
+
750
+ it('relationCreate auto-hides under morphedByMany (no per-pivot create surface)', async () => {
751
+ const result = await Action.relationCreate(Posts, morphedByManyCtx).evaluate({})
752
+ assert.equal(result.visible, false)
753
+ })
754
+
755
+ it('relationEdit auto-hides under morphedByMany (no per-pivot edit surface)', async () => {
756
+ const result = await Action.relationEdit(Posts, morphedByManyCtx).evaluate({ record: { id: '9' } })
757
+ assert.equal(result.visible, false)
758
+ })
759
+
760
+ it('relationDelete auto-hides under morphedByMany (use relationDetach instead)', async () => {
761
+ const result = await Action.relationDelete(Posts, morphedByManyCtx).evaluate({ record: { id: '9' } })
762
+ assert.equal(result.visible, false)
763
+ })
764
+ })
765
+ })
766
+
767
+ describe('Action soft-delete factories (Plan #13)', () => {
768
+ /** Minimal ResourceLike satisfying the Action factories. */
769
+ function makeR(over: Partial<{
770
+ softDeletes: boolean
771
+ deletedAtColumn: string
772
+ canDelete: (...args: unknown[]) => boolean | Promise<boolean>
773
+ canRestore: (...args: unknown[]) => boolean | Promise<boolean>
774
+ canForceDelete: (...args: unknown[]) => boolean | Promise<boolean>
775
+ }> = {}) {
776
+ return {
777
+ labelSingular: 'Post',
778
+ getSlug: () => 'posts',
779
+ ...(over.softDeletes !== undefined ? { softDeletes: over.softDeletes } : {}),
780
+ ...(over.deletedAtColumn !== undefined ? { deletedAtColumn: over.deletedAtColumn } : {}),
781
+ ...(over.canDelete ? { canDelete: over.canDelete } : {}),
782
+ ...(over.canRestore ? { canRestore: over.canRestore } : {}),
783
+ ...(over.canForceDelete ? { canForceDelete: over.canForceDelete } : {}),
784
+ }
785
+ }
786
+
787
+ describe('Action.delete trashed-row visibility', () => {
788
+ it('hides on already-trashed rows when softDeletes=true', async () => {
789
+ const R = makeR({ softDeletes: true })
790
+ const a = Action.delete(R, '/admin')
791
+ const r1 = await a.evaluate({ record: { id: '7', deletedAt: '2026-01-01' } })
792
+ assert.equal(r1.visible, false)
793
+ })
794
+
795
+ it('shows on live rows when softDeletes=true and canDelete allows', async () => {
796
+ const R = makeR({ softDeletes: true })
797
+ const a = Action.delete(R, '/admin')
798
+ const r1 = await a.evaluate({ record: { id: '7' } })
799
+ assert.equal(r1.visible, true)
800
+ })
801
+
802
+ it('ignores deletedAt entirely when softDeletes is not set', async () => {
803
+ const R = makeR()
804
+ const a = Action.delete(R, '/admin')
805
+ // Even with deletedAt set, the regular delete should show — non-soft-delete
806
+ // resources don't gate on the column.
807
+ const r1 = await a.evaluate({ record: { id: '7', deletedAt: '2026-01-01' } })
808
+ assert.equal(r1.visible, true)
809
+ })
810
+
811
+ it('honors a custom deletedAtColumn', async () => {
812
+ const R = makeR({ softDeletes: true, deletedAtColumn: 'archivedAt' })
813
+ const a = Action.delete(R, '/admin')
814
+ assert.equal((await a.evaluate({ record: { archivedAt: '2026-01-01' } })).visible, false)
815
+ assert.equal((await a.evaluate({ record: { archivedAt: null } })).visible, true)
816
+ })
817
+ })
818
+
819
+ describe('Action.restore', () => {
820
+ it('builds the restore URL with :id template', () => {
821
+ const meta = Action.restore(makeR({ softDeletes: true }), '/admin').toMeta()
822
+ assert.equal(meta.method, 'post')
823
+ assert.equal(meta.action, '/admin/posts/:id/restore')
824
+ assert.equal(meta.label, 'Restore')
825
+ assert.equal(meta.color, 'success')
826
+ })
827
+
828
+ it('hides on live rows', async () => {
829
+ const a = Action.restore(makeR({ softDeletes: true }), '/admin')
830
+ assert.equal((await a.evaluate({ record: { id: '7' } })).visible, false)
831
+ })
832
+
833
+ it('shows on trashed rows when canRestore allows', async () => {
834
+ const a = Action.restore(makeR({ softDeletes: true }), '/admin')
835
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, true)
836
+ })
837
+
838
+ it('hides on trashed rows when canRestore denies', async () => {
839
+ const a = Action.restore(makeR({ softDeletes: true, canRestore: async () => false }), '/admin')
840
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, false)
841
+ })
842
+
843
+ it('honors explicit recordId at config time', () => {
844
+ const meta = Action.restore(makeR({ softDeletes: true }), '/admin', '7').toMeta()
845
+ assert.equal(meta.action, '/admin/posts/7/restore')
846
+ })
847
+ })
848
+
849
+ describe('Action.forceDelete', () => {
850
+ it('builds the force-delete URL with destructive style + permanence confirm', () => {
851
+ const meta = Action.forceDelete(makeR({ softDeletes: true }), '/admin').toMeta()
852
+ assert.equal(meta.method, 'post')
853
+ assert.equal(meta.action, '/admin/posts/:id/force-delete')
854
+ assert.equal(meta.destructive, true)
855
+ assert.match(meta.confirm?.message ?? '', /cannot be undone/i)
856
+ })
857
+
858
+ it('hides on live rows', async () => {
859
+ const a = Action.forceDelete(makeR({ softDeletes: true }), '/admin')
860
+ assert.equal((await a.evaluate({ record: { id: '7' } })).visible, false)
861
+ })
862
+
863
+ it('shows on trashed rows when canForceDelete allows', async () => {
864
+ const a = Action.forceDelete(makeR({ softDeletes: true }), '/admin')
865
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, true)
866
+ })
867
+
868
+ it('hides on trashed rows when canForceDelete denies', async () => {
869
+ const a = Action.forceDelete(makeR({ softDeletes: true, canForceDelete: async () => false }), '/admin')
870
+ assert.equal((await a.evaluate({ record: { deletedAt: '2026-01-01' } })).visible, false)
871
+ })
872
+
873
+ it('label is "Delete forever" — distinguishes from regular delete', () => {
874
+ assert.equal(Action.forceDelete(makeR({ softDeletes: true }), '/admin').toMeta().label, 'Delete forever')
875
+ })
876
+ })
877
+ })
878
+
879
+ describe('Action bulk soft-delete factories (Plan #13)', () => {
880
+ it('bulkDelete iterates records and calls deleteRecord, returns count notification', async () => {
881
+ const deleted: string[] = []
882
+ const R = {
883
+ labelSingular: 'Post',
884
+ getSlug: () => 'posts',
885
+ softDeletes: true,
886
+ deletedAtColumn: 'deletedAt',
887
+ async deleteRecord(id: string) { deleted.push(id) },
888
+ } as never
889
+ const a = Action.bulkDelete(R, '/admin')
890
+ const meta = a.toMeta()
891
+ assert.equal(meta.placement, 'bulk')
892
+ assert.equal(meta.destructive, true)
893
+
894
+ const handler = a.getHandler()!
895
+ const result = await handler({
896
+ records: [{ id: '1' }, { id: '2' }, { id: '3' }],
897
+ user: null,
898
+ })
899
+ assert.deepEqual(deleted.sort(), ['1', '2', '3'])
900
+ const notify = (result as { notify: { title: string } }).notify
901
+ assert.match(notify.title, /3 posts moved to trash/i)
902
+ })
903
+
904
+ it('bulkDelete uses "deleted" verb for non-soft-delete resources', async () => {
905
+ const R = {
906
+ labelSingular: 'Post',
907
+ getSlug: () => 'posts',
908
+ // softDeletes: false
909
+ async deleteRecord() { /* no-op */ },
910
+ } as never
911
+ const handler = Action.bulkDelete(R, '/admin').getHandler()!
912
+ const result = await handler({ records: [{ id: '1' }], user: null })
913
+ // Count-aware singular: 1 → labelSingular, not the naive plural.
914
+ assert.match((result as { notify: { title: string } }).notify.title, /1 post deleted/)
915
+ })
916
+
917
+ it('bulkDelete uses the count-aware singular form when n=1', async () => {
918
+ const R = {
919
+ labelSingular: 'Article',
920
+ label: 'Articles', // explicit plural
921
+ getSlug: () => 'articles',
922
+ softDeletes: true,
923
+ async deleteRecord() { /* no-op */ },
924
+ } as never
925
+ const handler = Action.bulkDelete(R, '/admin').getHandler()!
926
+ const r1 = await handler({ records: [{ id: '1' }], user: null })
927
+ assert.match((r1 as { notify: { title: string } }).notify.title, /^1 article moved to trash$/)
928
+ const r5 = await handler({ records: Array.from({ length: 5 }, (_, i) => ({ id: String(i) })), user: null })
929
+ assert.match((r5 as { notify: { title: string } }).notify.title, /^5 articles moved to trash$/)
930
+ })
931
+
932
+ it('bulkDelete falls back to naive ${labelSingular}s when no plural label is set', async () => {
933
+ const R = {
934
+ labelSingular: 'Post',
935
+ // No `label` set — uses fallback.
936
+ getSlug: () => 'posts',
937
+ softDeletes: true,
938
+ async deleteRecord() { /* no-op */ },
939
+ } as never
940
+ const handler = Action.bulkDelete(R, '/admin').getHandler()!
941
+ const r5 = await handler({ records: Array.from({ length: 5 }, (_, i) => ({ id: String(i) })), user: null })
942
+ assert.match((r5 as { notify: { title: string } }).notify.title, /5 posts moved to trash/)
943
+ })
944
+
945
+ it('bulkDelete skips rows whose canDelete returns false', async () => {
946
+ const deleted: string[] = []
947
+ const R = {
948
+ labelSingular: 'Post',
949
+ getSlug: () => 'posts',
950
+ async canDelete(_user: unknown, record: unknown) {
951
+ return (record as { id: string }).id !== '2' // deny id 2
952
+ },
953
+ async deleteRecord(id: string) { deleted.push(id) },
954
+ } as never
955
+ const handler = Action.bulkDelete(R, '/admin').getHandler()!
956
+ const result = await handler({
957
+ records: [{ id: '1' }, { id: '2' }, { id: '3' }],
958
+ user: null,
959
+ })
960
+ assert.deepEqual(deleted.sort(), ['1', '3'])
961
+ assert.match((result as { notify: { title: string } }).notify.title, /2 posts/)
962
+ })
963
+
964
+ it('bulkRestore calls model.restore on each row', async () => {
965
+ const restored: string[] = []
966
+ const R = {
967
+ labelSingular: 'Post',
968
+ getSlug: () => 'posts',
969
+ softDeletes: true,
970
+ deletedAtColumn: 'deletedAt',
971
+ model: {
972
+ async restore(id: string | number) { restored.push(String(id)); return {} },
973
+ },
974
+ } as never
975
+ const handler = Action.bulkRestore(R, '/admin').getHandler()!
976
+ const result = await handler({ records: [{ id: '1' }, { id: '2' }], user: null })
977
+ assert.deepEqual(restored.sort(), ['1', '2'])
978
+ assert.match((result as { notify: { title: string } }).notify.title, /2 posts restored/i)
979
+ })
980
+
981
+ it('bulkRestore returns an error notify when model.restore is missing', async () => {
982
+ const R = {
983
+ labelSingular: 'Post',
984
+ getSlug: () => 'posts',
985
+ softDeletes: true,
986
+ model: {},
987
+ } as never
988
+ const handler = Action.bulkRestore(R, '/admin').getHandler()!
989
+ const result = await handler({ records: [{ id: '1' }], user: null })
990
+ const notify = (result as { notify: { title: string; type: string } }).notify
991
+ assert.match(notify.title, /not configured/i)
992
+ assert.equal(notify.type, 'error')
993
+ })
994
+
995
+ it('bulkForceDelete calls model.forceDelete on each row', async () => {
996
+ const purged: string[] = []
997
+ const R = {
998
+ labelSingular: 'Post',
999
+ getSlug: () => 'posts',
1000
+ softDeletes: true,
1001
+ deletedAtColumn: 'deletedAt',
1002
+ model: {
1003
+ async forceDelete(id: string | number) { purged.push(String(id)) },
1004
+ },
1005
+ } as never
1006
+ const handler = Action.bulkForceDelete(R, '/admin').getHandler()!
1007
+ const result = await handler({ records: [{ id: '1' }, { id: '2' }], user: null })
1008
+ assert.deepEqual(purged.sort(), ['1', '2'])
1009
+ assert.match((result as { notify: { title: string } }).notify.title, /2 posts permanently deleted/i)
1010
+ })
1011
+
1012
+ it('all three bulk factories ship the correct placement + destructive flags', () => {
1013
+ const R = { labelSingular: 'Post', getSlug: () => 'posts' } as never
1014
+ const del = Action.bulkDelete(R, '/admin').toMeta()
1015
+ const restore = Action.bulkRestore(R, '/admin').toMeta()
1016
+ const fdelete = Action.bulkForceDelete(R, '/admin').toMeta()
1017
+
1018
+ assert.equal(del.placement, 'bulk')
1019
+ assert.equal(restore.placement, 'bulk')
1020
+ assert.equal(fdelete.placement, 'bulk')
1021
+
1022
+ assert.equal(del.destructive, true)
1023
+ assert.equal(restore.destructive, false)
1024
+ assert.equal(fdelete.destructive, true)
1025
+
1026
+ assert.equal(restore.color, 'success')
1027
+ })
1028
+ })
1029
+
1030
+ describe('Action.replicate factory', () => {
1031
+ function makeR(over: Partial<{
1032
+ primaryKey: string
1033
+ deletedAtColumn: string
1034
+ canCreate: (...args: unknown[]) => boolean | Promise<boolean>
1035
+ create: (data: Record<string, unknown>) => Promise<unknown>
1036
+ }> = {}): never {
1037
+ const created: Array<Record<string, unknown>> = []
1038
+ const R = {
1039
+ labelSingular: 'Post',
1040
+ getSlug: () => 'posts',
1041
+ ...(over.deletedAtColumn !== undefined ? { deletedAtColumn: over.deletedAtColumn } : {}),
1042
+ ...(over.canCreate ? { canCreate: over.canCreate } : {}),
1043
+ model: {
1044
+ ...(over.primaryKey !== undefined ? { primaryKey: over.primaryKey } : {}),
1045
+ async create(data: Record<string, unknown>) {
1046
+ created.push(data)
1047
+ return over.create ? await over.create(data) : { id: '99', ...data }
1048
+ },
1049
+ },
1050
+ _created: created,
1051
+ } as never
1052
+ return R
1053
+ }
1054
+
1055
+ it('creates a duplicate via R.model.create with the source record minus PK', async () => {
1056
+ const R = makeR()
1057
+ const handler = Action.replicate(R, '/admin').getHandler()!
1058
+ const result = await handler({
1059
+ record: { id: '7', title: 'Hello', body: 'World', deletedAt: null },
1060
+ user: null,
1061
+ })
1062
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1063
+ assert.equal(created.length, 1)
1064
+ assert.equal(created[0]!['title'], 'Hello')
1065
+ assert.equal(created[0]!['body'], 'World')
1066
+ assert.equal(created[0]!['id'], undefined, 'PK must be stripped')
1067
+ assert.equal(created[0]!['deletedAt'], undefined, 'soft-delete column stripped')
1068
+ const r = result as { redirect: string; notify: { title: string; type: string } }
1069
+ assert.equal(r.redirect, '/admin/posts/99/edit')
1070
+ assert.match(r.notify.title, /Post replicated/)
1071
+ assert.equal(r.notify.type, 'success')
1072
+ })
1073
+
1074
+ it('honors a non-default primary key column', async () => {
1075
+ const R = makeR({ primaryKey: 'uuid', create: async (d) => ({ uuid: 'abc-123', ...d }) })
1076
+ const handler = Action.replicate(R, '/admin').getHandler()!
1077
+ const result = await handler({
1078
+ record: { uuid: 'src-1', title: 'Hello' },
1079
+ user: null,
1080
+ })
1081
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1082
+ assert.equal(created[0]!['uuid'], undefined)
1083
+ assert.equal((result as { redirect: string }).redirect, '/admin/posts/abc-123/edit')
1084
+ })
1085
+
1086
+ it('honors a custom deletedAtColumn', async () => {
1087
+ const R = makeR({ deletedAtColumn: 'archivedAt' })
1088
+ const handler = Action.replicate(R, '/admin').getHandler()!
1089
+ await handler({
1090
+ record: { id: '1', title: 'X', archivedAt: '2026-01-01', deletedAt: 'should-pass' },
1091
+ user: null,
1092
+ })
1093
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1094
+ assert.equal(created[0]!['archivedAt'], undefined, 'custom soft-delete column stripped')
1095
+ assert.equal(created[0]!['deletedAt'], 'should-pass', 'default name no longer special when custom is set')
1096
+ })
1097
+
1098
+ it('drops opts.excludeAttributes from the replica', async () => {
1099
+ const R = makeR()
1100
+ const handler = Action.replicate(R, '/admin', undefined, { excludeAttributes: ['slug', 'email'] }).getHandler()!
1101
+ await handler({
1102
+ record: { id: '1', title: 'Hello', slug: 'hello', email: 'a@b.co' },
1103
+ user: null,
1104
+ })
1105
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1106
+ assert.equal(created[0]!['slug'], undefined)
1107
+ assert.equal(created[0]!['email'], undefined)
1108
+ assert.equal(created[0]!['title'], 'Hello')
1109
+ })
1110
+
1111
+ it('runs opts.beforeReplicaSaved to mutate the prepared payload', async () => {
1112
+ const R = makeR()
1113
+ const handler = Action.replicate(R, '/admin', undefined, {
1114
+ beforeReplicaSaved: (replica) => ({ ...replica, title: `Copy of ${replica['title']}` }),
1115
+ }).getHandler()!
1116
+ await handler({ record: { id: '1', title: 'Hello' }, user: null })
1117
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1118
+ assert.equal(created[0]!['title'], 'Copy of Hello')
1119
+ })
1120
+
1121
+ it('returns an error notify when R.model is missing', async () => {
1122
+ const R = { labelSingular: 'Post', getSlug: () => 'posts' } as never
1123
+ const handler = Action.replicate(R, '/admin').getHandler()!
1124
+ const result = await handler({ record: { id: '1' }, user: null })
1125
+ const notify = (result as { notify: { title: string; type: string } }).notify
1126
+ assert.match(notify.title, /not configured/i)
1127
+ assert.equal(notify.type, 'error')
1128
+ })
1129
+
1130
+ it('returns an error notify when ctx.record is missing', async () => {
1131
+ const R = makeR()
1132
+ const handler = Action.replicate(R, '/admin').getHandler()!
1133
+ const result = await handler({ user: null })
1134
+ const notify = (result as { notify: { title: string; type: string } }).notify
1135
+ assert.match(notify.title, /source record missing/i)
1136
+ })
1137
+
1138
+ it('catches errors thrown by R.model.create and surfaces them', async () => {
1139
+ const R = makeR({ create: async () => { throw new Error('unique constraint failed: posts.slug') } })
1140
+ const handler = Action.replicate(R, '/admin').getHandler()!
1141
+ const result = await handler({ record: { id: '1', slug: 'x' }, user: null })
1142
+ const notify = (result as { notify: { title: string; type: string } }).notify
1143
+ assert.match(notify.title, /unique constraint/i)
1144
+ assert.equal(notify.type, 'error')
1145
+ })
1146
+
1147
+ it('visibility delegates to R.canCreate', async () => {
1148
+ const R1 = makeR({ canCreate: () => true })
1149
+ const R2 = makeR({ canCreate: () => false })
1150
+ assert.equal((await Action.replicate(R1, '/admin').evaluate({})).visible, true)
1151
+ assert.equal((await Action.replicate(R2, '/admin').evaluate({})).visible, false)
1152
+ })
1153
+
1154
+ it('falls back to list page when create() returns no PK on the new record', async () => {
1155
+ const R = makeR({ create: async () => ({}) })
1156
+ const handler = Action.replicate(R, '/admin').getHandler()!
1157
+ const result = await handler({ record: { id: '1', title: 'Hello' }, user: null })
1158
+ assert.equal((result as { redirect: string }).redirect, '/admin/posts')
1159
+ })
1160
+
1161
+ it('opts.getCreatedNotificationTitle overrides the default success title', async () => {
1162
+ const R = makeR({ create: async (d) => ({ id: '99', ...d }) })
1163
+ let seenReplica: unknown
1164
+ let seenSource: unknown
1165
+ const handler = Action.replicate(R, '/admin', undefined, {
1166
+ getCreatedNotificationTitle: ({ replica, source }) => {
1167
+ seenReplica = replica
1168
+ seenSource = source
1169
+ return `Cloned "${(source as { title?: string })?.title}"`
1170
+ },
1171
+ }).getHandler()!
1172
+ const result = await handler({ record: { id: '7', title: 'Hello' }, user: null })
1173
+ assert.equal((result as { notify: { title: string } }).notify.title, 'Cloned "Hello"')
1174
+ assert.deepEqual(seenSource, { id: '7', title: 'Hello' })
1175
+ assert.deepEqual(seenReplica, { id: '99', title: 'Hello' })
1176
+ })
1177
+
1178
+ it('opts.getCreatedNotificationTitle returning undefined falls back to default', async () => {
1179
+ const R = makeR()
1180
+ const handler = Action.replicate(R, '/admin', undefined, {
1181
+ getCreatedNotificationTitle: () => undefined,
1182
+ }).getHandler()!
1183
+ const result = await handler({ record: { id: '1', title: 'X' }, user: null })
1184
+ assert.match((result as { notify: { title: string } }).notify.title, /^Post replicated$/)
1185
+ })
1186
+
1187
+ it('opts.getRedirectUrl overrides the default new-record edit URL', async () => {
1188
+ const R = makeR()
1189
+ const handler = Action.replicate(R, '/admin', undefined, {
1190
+ getRedirectUrl: ({ replica }) => `/admin/posts/${(replica as { id: string }).id}/preview`,
1191
+ }).getHandler()!
1192
+ const result = await handler({ record: { id: '1', title: 'Hello' }, user: null })
1193
+ assert.equal((result as { redirect: string }).redirect, '/admin/posts/99/preview')
1194
+ })
1195
+
1196
+ it('opts.getRedirectUrl returning undefined falls back to default', async () => {
1197
+ const R = makeR()
1198
+ const handler = Action.replicate(R, '/admin', undefined, {
1199
+ getRedirectUrl: () => undefined,
1200
+ }).getHandler()!
1201
+ const result = await handler({ record: { id: '1', title: 'Hello' }, user: null })
1202
+ assert.equal((result as { redirect: string }).redirect, '/admin/posts/99/edit')
1203
+ })
1204
+
1205
+ it('opts.getRedirectUrl honors an explicit empty string (not swallowed by ??)', async () => {
1206
+ const R = makeR()
1207
+ const handler = Action.replicate(R, '/admin', undefined, {
1208
+ getRedirectUrl: () => '',
1209
+ }).getHandler()!
1210
+ const result = await handler({ record: { id: '1', title: 'Hello' }, user: null })
1211
+ assert.equal((result as { redirect: string }).redirect, '')
1212
+ })
1213
+
1214
+ it('overrides may be async', async () => {
1215
+ const R = makeR()
1216
+ const handler = Action.replicate(R, '/admin', undefined, {
1217
+ getCreatedNotificationTitle: async () => 'async title',
1218
+ getRedirectUrl: async () => '/admin/elsewhere',
1219
+ }).getHandler()!
1220
+ const result = await handler({ record: { id: '1', title: 'Hello' }, user: null }) as {
1221
+ redirect: string
1222
+ notify: { title: string }
1223
+ }
1224
+ assert.equal(result.notify.title, 'async title')
1225
+ assert.equal(result.redirect, '/admin/elsewhere')
1226
+ })
1227
+ })
1228
+
1229
+ describe('Action.bulkReplicate factory', () => {
1230
+ function makeR(over: Partial<{
1231
+ primaryKey: string
1232
+ deletedAtColumn: string
1233
+ canCreate: (...args: unknown[]) => boolean | Promise<boolean>
1234
+ create: (data: Record<string, unknown>) => Promise<unknown>
1235
+ }> = {}): never {
1236
+ const created: Array<Record<string, unknown>> = []
1237
+ const R = {
1238
+ labelSingular: 'Post',
1239
+ label: 'Posts',
1240
+ getSlug: () => 'posts',
1241
+ ...(over.deletedAtColumn !== undefined ? { deletedAtColumn: over.deletedAtColumn } : {}),
1242
+ ...(over.canCreate ? { canCreate: over.canCreate } : {}),
1243
+ model: {
1244
+ ...(over.primaryKey !== undefined ? { primaryKey: over.primaryKey } : {}),
1245
+ async create(data: Record<string, unknown>) {
1246
+ created.push(data)
1247
+ return over.create ? await over.create(data) : { id: String(created.length), ...data }
1248
+ },
1249
+ },
1250
+ _created: created,
1251
+ } as never
1252
+ return R
1253
+ }
1254
+
1255
+ it('renders as a bulk action with confirm prompt', () => {
1256
+ const R = makeR()
1257
+ const meta = Action.bulkReplicate(R, '/admin').toMeta()
1258
+ assert.equal(meta.placement, 'bulk')
1259
+ assert.match(meta.confirm?.message ?? '', /Replicate the selected/)
1260
+ })
1261
+
1262
+ it('iterates ctx.records and creates one row per source', async () => {
1263
+ const R = makeR()
1264
+ const handler = Action.bulkReplicate(R, '/admin').getHandler()!
1265
+ const result = await handler({
1266
+ records: [
1267
+ { id: '1', title: 'A' },
1268
+ { id: '2', title: 'B' },
1269
+ { id: '3', title: 'C' },
1270
+ ],
1271
+ user: null,
1272
+ })
1273
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1274
+ assert.equal(created.length, 3)
1275
+ assert.deepEqual(created.map(r => r['title']), ['A', 'B', 'C'])
1276
+ assert.match((result as { notify: { title: string } }).notify.title, /3 posts replicated/)
1277
+ })
1278
+
1279
+ it('strips PK + soft-delete + excludeAttributes from each replica', async () => {
1280
+ const R = makeR({ primaryKey: 'uuid', deletedAtColumn: 'archivedAt' })
1281
+ const handler = Action.bulkReplicate(R, '/admin', {
1282
+ excludeAttributes: ['slug'],
1283
+ }).getHandler()!
1284
+ await handler({
1285
+ records: [
1286
+ { uuid: 'a', title: 'X', slug: 'x', archivedAt: null },
1287
+ { uuid: 'b', title: 'Y', slug: 'y', archivedAt: '2026-01-01' },
1288
+ ],
1289
+ user: null,
1290
+ })
1291
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1292
+ for (const r of created) {
1293
+ assert.equal(r['uuid'], undefined)
1294
+ assert.equal(r['slug'], undefined)
1295
+ assert.equal(r['archivedAt'], undefined)
1296
+ }
1297
+ })
1298
+
1299
+ it('runs beforeReplicaSaved per row', async () => {
1300
+ const R = makeR()
1301
+ const handler = Action.bulkReplicate(R, '/admin', {
1302
+ beforeReplicaSaved: (replica) => ({ ...replica, title: `Copy of ${replica['title']}` }),
1303
+ }).getHandler()!
1304
+ await handler({
1305
+ records: [{ id: '1', title: 'A' }, { id: '2', title: 'B' }],
1306
+ user: null,
1307
+ })
1308
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1309
+ assert.deepEqual(created.map(r => r['title']), ['Copy of A', 'Copy of B'])
1310
+ })
1311
+
1312
+ it('skips rows whose canCreate returns false', async () => {
1313
+ let calls = 0
1314
+ const R = makeR({ canCreate: async () => { calls++; return calls !== 2 } })
1315
+ const handler = Action.bulkReplicate(R, '/admin').getHandler()!
1316
+ const result = await handler({
1317
+ records: [{ id: '1', title: 'A' }, { id: '2', title: 'B' }, { id: '3', title: 'C' }],
1318
+ user: null,
1319
+ })
1320
+ const created = (R as unknown as { _created: Array<Record<string, unknown>> })._created
1321
+ assert.equal(created.length, 2)
1322
+ assert.match((result as { notify: { title: string } }).notify.title, /^2 posts replicated$/)
1323
+ })
1324
+
1325
+ it('skips rows where create throws and reports only successful count', async () => {
1326
+ let i = 0
1327
+ const R = makeR({ create: async () => {
1328
+ i++
1329
+ if (i === 2) throw new Error('boom')
1330
+ return { id: String(i) }
1331
+ } })
1332
+ const handler = Action.bulkReplicate(R, '/admin').getHandler()!
1333
+ const result = await handler({
1334
+ records: [{ id: '1' }, { id: '2' }, { id: '3' }],
1335
+ user: null,
1336
+ })
1337
+ assert.match((result as { notify: { title: string } }).notify.title, /^2 posts replicated$/)
1338
+ })
1339
+
1340
+ it('returns an error notify when R.model.create is missing', async () => {
1341
+ const R = { labelSingular: 'Post', getSlug: () => 'posts' } as never
1342
+ const handler = Action.bulkReplicate(R, '/admin').getHandler()!
1343
+ const result = await handler({ records: [{ id: '1' }], user: null })
1344
+ const notify = (result as { notify: { title: string; type: string } }).notify
1345
+ assert.match(notify.title, /not configured/i)
1346
+ assert.equal(notify.type, 'error')
1347
+ })
1348
+
1349
+ it('count-aware singular form when n=1', async () => {
1350
+ const R = makeR()
1351
+ const handler = Action.bulkReplicate(R, '/admin').getHandler()!
1352
+ const result = await handler({ records: [{ id: '1', title: 'A' }], user: null })
1353
+ assert.match((result as { notify: { title: string } }).notify.title, /^1 post replicated$/)
1354
+ })
1355
+
1356
+ it('visibility delegates to R.canCreate', async () => {
1357
+ const R1 = makeR({ canCreate: () => true })
1358
+ const R2 = makeR({ canCreate: () => false })
1359
+ assert.equal((await Action.bulkReplicate(R1, '/admin').evaluate({})).visible, true)
1360
+ assert.equal((await Action.bulkReplicate(R2, '/admin').evaluate({})).visible, false)
1361
+ })
1362
+
1363
+ it('opts.getCreatedNotificationTitle receives count + sources and overrides default', async () => {
1364
+ const R = makeR()
1365
+ let seenCount: unknown
1366
+ let seenRecords: unknown
1367
+ const handler = Action.bulkReplicate(R, '/admin', {
1368
+ getCreatedNotificationTitle: ({ count, records }) => {
1369
+ seenCount = count
1370
+ seenRecords = records
1371
+ return `Duplicated ${count} of ${(records as unknown[]).length}`
1372
+ },
1373
+ }).getHandler()!
1374
+ const result = await handler({
1375
+ records: [{ id: '1' }, { id: '2' }, { id: '3' }],
1376
+ user: null,
1377
+ })
1378
+ assert.equal((result as { notify: { title: string } }).notify.title, 'Duplicated 3 of 3')
1379
+ assert.equal(seenCount, 3)
1380
+ assert.deepEqual(seenRecords, [{ id: '1' }, { id: '2' }, { id: '3' }])
1381
+ })
1382
+
1383
+ it('opts.getCreatedNotificationTitle returning undefined falls back to default', async () => {
1384
+ const R = makeR()
1385
+ const handler = Action.bulkReplicate(R, '/admin', {
1386
+ getCreatedNotificationTitle: () => undefined,
1387
+ }).getHandler()!
1388
+ const result = await handler({ records: [{ id: '1' }], user: null })
1389
+ assert.match((result as { notify: { title: string } }).notify.title, /^1 post replicated$/)
1390
+ })
1391
+ })
1392
+
1393
+ describe('Action visibility through resolveSchema (non-row placements)', () => {
1394
+ it('drops a header action when visible() returns false', async () => {
1395
+ const tree = [
1396
+ Action.make('hidden').header().visible(false),
1397
+ Action.make('shown').header(),
1398
+ ]
1399
+ const result = await resolveSchema(tree)
1400
+ assert.equal(result.length, 1)
1401
+ assert.equal(result[0]!['name'], 'shown')
1402
+ })
1403
+
1404
+ it('keeps row-placement actions in the tree even when hidden — per-row eval handles them', async () => {
1405
+ const tree = [Action.make('rowAction').row().visible(false)]
1406
+ const result = await resolveSchema(tree)
1407
+ assert.equal(result.length, 1, 'row actions are always serialized; per-row eval filters at render time')
1408
+ assert.equal(result[0]!['conditional'], true, 'conditional flag tells the row renderer to consult the lookup')
1409
+ })
1410
+
1411
+ it('stamps disabled:true on header action when disabled(true) is set', async () => {
1412
+ const result = await resolveSchema([Action.make('a').header().disabled(true)])
1413
+ assert.equal(result[0]!['disabled'], true)
1414
+ })
1415
+ })
1416
+
1417
+ describe('Action.export factory', () => {
1418
+ /** Lazily-resolved Column class — Column module imports Action, so a
1419
+ * top-level static import would tighten the cycle. */
1420
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1421
+ let ColumnClass: any
1422
+ beforeEach(async () => {
1423
+ if (!ColumnClass) ColumnClass = (await import('../Column.js')).Column
1424
+ })
1425
+
1426
+ /** Build a minimal Resource-like object with an opt-in `table()`
1427
+ * configurator and visibility predicate. */
1428
+ function makeR(over: {
1429
+ canViewAny?: (user: unknown) => boolean | Promise<boolean>
1430
+ rows?: Record<string, unknown>[]
1431
+ perPageHint?: number
1432
+ columns?: Array<string>
1433
+ } = {}) {
1434
+ const rows = over.rows ?? [
1435
+ { id: 1, name: 'Ada', email: 'ada@example.com' },
1436
+ { id: 2, name: 'Grace', email: 'grace@example.com' },
1437
+ ]
1438
+ const colNames = over.columns ?? ['id', 'name', 'email']
1439
+
1440
+ return {
1441
+ labelSingular: 'User',
1442
+ label: 'Users',
1443
+ getSlug: () => 'users',
1444
+ ...(over.canViewAny ? { canViewAny: over.canViewAny } : {}),
1445
+ // R.table is called inside the export handler with an empty Table.
1446
+ // We mutate the passed table in-place, mirroring how user code does
1447
+ // it via the fluent builder. Returning the same `t` matches the
1448
+ // contract of `static table(t)`.
1449
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1450
+ table(t: any) {
1451
+ const cols = colNames.map(n => ColumnClass.make(n))
1452
+ // Records handler — pages 1..N where N covers all rows. Honors
1453
+ // the `perPage` we ask for (so chunking exits cleanly).
1454
+ t.columns(cols)
1455
+ t.records(({ page = 1, perPage = 1000 }: { page?: number; perPage?: number }) => {
1456
+ const start = (page - 1) * perPage
1457
+ return { rows: rows.slice(start, start + perPage), total: rows.length }
1458
+ })
1459
+ return t
1460
+ },
1461
+ }
1462
+ }
1463
+
1464
+ it('returns a CSV download envelope with default filename / contentType', async () => {
1465
+ const a = Action.export(makeR(), '/admin')
1466
+ const result = await a.getHandler()!({ values: {} })
1467
+ const env = (result as { download?: { filename: string; contentType: string; body: string } }).download
1468
+ assert.ok(env, 'expected download envelope')
1469
+ assert.match(env!.filename, /^users-\d{4}-\d{2}-\d{2}\.csv$/)
1470
+ assert.equal(env!.contentType, 'text/csv; charset=utf-8')
1471
+ // Default label is Column.getLabel() which title-cases the key
1472
+ // (`id` → `Id`, `email` → `Email`). Tests pin the title-cased shape
1473
+ // so a Column.getLabel() change here is intentional.
1474
+ assert.equal(env!.body.split('\r\n')[0], 'Id,Name,Email')
1475
+ assert.ok(env!.body.includes('1,Ada,ada@example.com'))
1476
+ assert.ok(env!.body.includes('2,Grace,grace@example.com'))
1477
+ })
1478
+
1479
+ it('honors explicit columns option (string + object form)', async () => {
1480
+ const a = Action.export(makeR(), '/admin', {
1481
+ columns: ['id', { key: 'name', label: 'Full name' }],
1482
+ })
1483
+ const result = await a.getHandler()!({ values: {} })
1484
+ const body = (result as { download: { body: string } }).download.body
1485
+ assert.equal(body.split('\r\n')[0], 'id,Full name')
1486
+ assert.ok(body.includes('1,Ada'))
1487
+ assert.ok(!body.includes('email'))
1488
+ })
1489
+
1490
+ it('column.format(value, record) maps the cell before write', async () => {
1491
+ const a = Action.export(makeR(), '/admin', {
1492
+ columns: [
1493
+ { key: 'id' },
1494
+ { key: 'name', format: (v) => `<<${v}>>` },
1495
+ ],
1496
+ })
1497
+ const body = ((await a.getHandler()!({ values: {} })) as { download: { body: string } }).download.body
1498
+ assert.ok(body.includes('1,<<Ada>>'))
1499
+ })
1500
+
1501
+ it('honors filename option as string and as function', async () => {
1502
+ const a1 = Action.export(makeR(), '/admin', { filename: 'fixed.csv' })
1503
+ const env1 = ((await a1.getHandler()!({ values: {} })) as { download: { filename: string } }).download
1504
+ assert.equal(env1.filename, 'fixed.csv')
1505
+
1506
+ const a2 = Action.export(makeR(), '/admin', {
1507
+ filename: (ctx) => `dynamic-${(ctx.values as { v?: string }).v}.csv`,
1508
+ })
1509
+ const env2 = ((await a2.getHandler()!({ values: { v: '42' } })) as { download: { filename: string } }).download
1510
+ assert.equal(env2.filename, 'dynamic-42.csv')
1511
+ })
1512
+
1513
+ it('format: "json" returns JSON body + JSON content-type', async () => {
1514
+ const a = Action.export(makeR(), '/admin', { format: 'json' })
1515
+ const env = ((await a.getHandler()!({ values: {} })) as { download: { body: string; contentType: string; filename: string } }).download
1516
+ assert.equal(env.contentType, 'application/json')
1517
+ assert.match(env.filename, /\.json$/)
1518
+ const parsed = JSON.parse(env.body)
1519
+ assert.equal(parsed.length, 2)
1520
+ assert.equal(parsed[0].name, 'Ada')
1521
+ })
1522
+
1523
+ it('emits an error notification when maxRows is exceeded', async () => {
1524
+ const big: Record<string, unknown>[] = Array.from({ length: 5 }, (_, i) => ({ id: i, name: `n${i}` }))
1525
+ const a = Action.export(makeR({ rows: big, columns: ['id', 'name'] }), '/admin', { maxRows: 3, chunkSize: 2 })
1526
+ const r = await a.getHandler()!({ values: {} })
1527
+ const notify = (r as { notify?: { type: string; title: string } }).notify
1528
+ assert.equal(notify?.type, 'error')
1529
+ assert.match(notify!.title, /exceeded 3/)
1530
+ })
1531
+
1532
+ it('emits an error notification when the resource has no table()', async () => {
1533
+ const R = { labelSingular: 'X', getSlug: () => 'x' }
1534
+ const a = Action.export(R, '/admin')
1535
+ const r = await a.getHandler()!({ values: {} })
1536
+ const notify = (r as { notify?: { type: string; title: string } }).notify
1537
+ assert.equal(notify?.type, 'error')
1538
+ assert.match(notify!.title, /not configured/)
1539
+ })
1540
+
1541
+ it('emits an error notification when columns resolve to none', async () => {
1542
+ const R = makeR({ columns: [] })
1543
+ const a = Action.export(R, '/admin')
1544
+ const r = await a.getHandler()!({ values: {} })
1545
+ const notify = (r as { notify?: { type: string; title: string } }).notify
1546
+ assert.equal(notify?.type, 'error')
1547
+ assert.match(notify!.title, /no columns/)
1548
+ })
1549
+
1550
+ it('visibility delegates to R.canViewAny — denies hide the action', async () => {
1551
+ const a = Action.export(makeR({ canViewAny: async () => false }), '/admin')
1552
+ const ev = await a.evaluate({ user: { id: 'u' } })
1553
+ assert.equal(ev.visible, false)
1554
+ })
1555
+
1556
+ it('visibility allows when canViewAny is unset (predicate defaults to true)', async () => {
1557
+ const a = Action.export(makeR(), '/admin')
1558
+ const ev = await a.evaluate({ user: { id: 'u' } })
1559
+ assert.equal(ev.visible, true)
1560
+ })
1561
+ })
1562
+
1563
+ describe('Action.bulkExport factory', () => {
1564
+ function makeR(canViewAny?: (user: unknown) => boolean | Promise<boolean>) {
1565
+ return {
1566
+ labelSingular: 'User',
1567
+ label: 'Users',
1568
+ getSlug: () => 'users',
1569
+ ...(canViewAny ? { canViewAny } : {}),
1570
+ }
1571
+ }
1572
+
1573
+ it('exports ctx.records with default columns from the records themselves', async () => {
1574
+ const a = Action.bulkExport(makeR(), '/admin', {
1575
+ columns: ['id', 'name'],
1576
+ })
1577
+ const r = await a.getHandler()!({
1578
+ values: {},
1579
+ records: [{ id: 1, name: 'Ada' }, { id: 2, name: 'Grace' }],
1580
+ })
1581
+ const env = (r as { download: { filename: string; body: string; contentType: string } }).download
1582
+ assert.equal(env.contentType, 'text/csv; charset=utf-8')
1583
+ assert.equal(env.body.split('\r\n')[0], 'id,name')
1584
+ assert.ok(env.body.includes('1,Ada'))
1585
+ assert.ok(env.body.includes('2,Grace'))
1586
+ })
1587
+
1588
+ it('marks placement as bulk', () => {
1589
+ const a = Action.bulkExport(makeR(), '/admin', { columns: ['id'] })
1590
+ assert.equal(a.toMeta().placement, 'bulk')
1591
+ })
1592
+
1593
+ it('exceeds maxRows → error notification', async () => {
1594
+ const a = Action.bulkExport(makeR(), '/admin', {
1595
+ columns: ['id'],
1596
+ maxRows: 1,
1597
+ })
1598
+ const r = await a.getHandler()!({
1599
+ values: {},
1600
+ records: [{ id: 1 }, { id: 2 }],
1601
+ })
1602
+ const notify = (r as { notify?: { type: string; title: string } }).notify
1603
+ assert.equal(notify?.type, 'error')
1604
+ assert.match(notify!.title, /exceeded 1/)
1605
+ })
1606
+
1607
+ it('format: "json" works for bulk too', async () => {
1608
+ const a = Action.bulkExport(makeR(), '/admin', { columns: ['id'], format: 'json' })
1609
+ const r = await a.getHandler()!({
1610
+ values: {},
1611
+ records: [{ id: 1 }],
1612
+ })
1613
+ const env = (r as { download: { body: string } }).download
1614
+ assert.deepEqual(JSON.parse(env.body), [{ id: 1 }])
1615
+ })
1616
+
1617
+ it('honors empty records (still emits a header-only CSV)', async () => {
1618
+ const a = Action.bulkExport(makeR(), '/admin', { columns: ['id', 'name'] })
1619
+ const r = await a.getHandler()!({ values: {}, records: [] })
1620
+ const env = (r as { download: { body: string } }).download
1621
+ assert.equal(env.body, 'id,name')
1622
+ })
1623
+ })
1624
+
1625
+ describe('Action.import factory', () => {
1626
+ /** Build a stand-in `R.model` that records calls and lets tests
1627
+ * control whether `query().where().paginate()` finds an existing
1628
+ * row. */
1629
+ function makeModel(over: { existingByKey?: Record<string, Record<string, unknown>> } = {}) {
1630
+ const created: Array<Record<string, unknown>> = []
1631
+ const updated: Array<{ id: string; payload: Record<string, unknown> }> = []
1632
+ return {
1633
+ created,
1634
+ updated,
1635
+ async create(payload: Record<string, unknown>) {
1636
+ created.push(payload)
1637
+ return { id: String(created.length), ...payload }
1638
+ },
1639
+ async update(id: string, payload: Record<string, unknown>) {
1640
+ updated.push({ id, payload })
1641
+ return { id, ...payload }
1642
+ },
1643
+ query() {
1644
+ return {
1645
+ where(col: string, val: unknown) {
1646
+ return {
1647
+ async paginate() {
1648
+ const lookup = over.existingByKey ?? {}
1649
+ const key = `${col}=${String(val)}`
1650
+ const found = lookup[key]
1651
+ return { data: found ? [found] : [] }
1652
+ },
1653
+ }
1654
+ },
1655
+ }
1656
+ },
1657
+ }
1658
+ }
1659
+
1660
+ function makeR(over: {
1661
+ canCreate?: (user: unknown) => boolean | Promise<boolean>
1662
+ model?: ReturnType<typeof makeModel>
1663
+ } = {}) {
1664
+ return {
1665
+ labelSingular: 'User',
1666
+ label: 'Users',
1667
+ getSlug: () => 'users',
1668
+ ...(over.canCreate ? { canCreate: over.canCreate } : {}),
1669
+ model: over.model ?? makeModel(),
1670
+ }
1671
+ }
1672
+
1673
+ /** Stub `globalThis.fetch` to return a static body, restore after the test. */
1674
+ function withFetch(text: string, fn: () => Promise<void>): Promise<void> {
1675
+ const original = globalThis.fetch
1676
+ globalThis.fetch = (async () => ({
1677
+ ok: true,
1678
+ text: async () => text,
1679
+ })) as unknown as typeof fetch
1680
+ return fn().finally(() => { globalThis.fetch = original })
1681
+ }
1682
+
1683
+ it('auto-builds the modal schema with a FileUpload child by default', () => {
1684
+ const a = Action.import(makeR(), '/admin')
1685
+ assert.equal(a.toMeta().confirm, undefined, 'import is form-modal, not a confirm prompt')
1686
+ const schema = a.getSchema()
1687
+ assert.equal(schema.length, 1, 'default modal: just the FileUpload')
1688
+ assert.equal((schema[0] as { name?: unknown }).name, 'file')
1689
+ })
1690
+
1691
+ it('appends a Mode select when upsertBy is set', async () => {
1692
+ const a = Action.import(makeR(), '/admin', { upsertBy: 'email' })
1693
+ // Modal heading preserved
1694
+ const modal = (a.toMeta() as unknown as { modal?: { heading?: string } }).modal
1695
+ assert.equal(modal?.heading, 'Import Users')
1696
+ // Schema is set via .schema() — read it via the same mechanism dispatchAction uses.
1697
+ const schema = a.getSchema()
1698
+ const names = schema.map(el => (el as { name?: unknown }).name)
1699
+ assert.deepEqual(names, ['file', 'mode'])
1700
+ })
1701
+
1702
+ it('CSV happy-path → all rows create through R.model.create', async () => {
1703
+ const model = makeModel()
1704
+ const R = makeR({ model })
1705
+ const a = Action.import(R, '/admin')
1706
+ await withFetch('name,email\r\nAda,ada@x\r\nGrace,grace@x\r\n', async () => {
1707
+ const r = await a.getHandler()!({
1708
+ values: { file: 'http://localhost/uploads/1.csv' },
1709
+ })
1710
+ assert.equal(model.created.length, 2)
1711
+ assert.deepEqual(model.created[0], { name: 'Ada', email: 'ada@x' })
1712
+ assert.deepEqual(model.created[1], { name: 'Grace', email: 'grace@x' })
1713
+ const notify = (r as { notify?: { type: string; title: string } }).notify
1714
+ assert.equal(notify?.type, 'success')
1715
+ assert.match(notify!.title, /2 created/)
1716
+ })
1717
+ })
1718
+
1719
+ it('upsertBy + matching row → R.model.update; non-matching → R.model.create', async () => {
1720
+ const model = makeModel({
1721
+ existingByKey: { 'email=ada@x': { id: '7', name: 'Old', email: 'ada@x' } },
1722
+ })
1723
+ const R = makeR({ model })
1724
+ const a = Action.import(R, '/admin', { upsertBy: 'email' })
1725
+ await withFetch('name,email\r\nAda,ada@x\r\nGrace,grace@x\r\n', async () => {
1726
+ const r = await a.getHandler()!({
1727
+ values: { file: 'http://localhost/uploads/1.csv', mode: 'upsert' },
1728
+ })
1729
+ assert.equal(model.updated.length, 1, 'one match → one update')
1730
+ assert.equal(model.updated[0]!.id, '7')
1731
+ assert.equal(model.created.length, 1, 'non-match → create')
1732
+ const notify = (r as { notify?: { title: string } }).notify
1733
+ assert.match(notify!.title, /1 created.*1 updated/)
1734
+ })
1735
+ })
1736
+
1737
+ it('upsertBy with mode=create → never updates, always creates', async () => {
1738
+ const model = makeModel({
1739
+ existingByKey: { 'email=ada@x': { id: '7', name: 'Old', email: 'ada@x' } },
1740
+ })
1741
+ const R = makeR({ model })
1742
+ const a = Action.import(R, '/admin', { upsertBy: 'email' })
1743
+ await withFetch('name,email\r\nAda,ada@x\r\n', async () => {
1744
+ await a.getHandler()!({
1745
+ values: { file: 'http://localhost/uploads/1.csv', mode: 'create' },
1746
+ })
1747
+ assert.equal(model.updated.length, 0)
1748
+ assert.equal(model.created.length, 1)
1749
+ })
1750
+ })
1751
+
1752
+ it('per-row validate() returning a string skips the row + accumulates as error', async () => {
1753
+ const model = makeModel()
1754
+ const R = makeR({ model })
1755
+ const a = Action.import(R, '/admin', {
1756
+ validate: (row) => (row['name'] === 'Ada' ? 'ada is reserved' : null),
1757
+ })
1758
+ await withFetch('name,email\r\nAda,ada@x\r\nGrace,grace@x\r\n', async () => {
1759
+ const r = await a.getHandler()!({
1760
+ values: { file: 'http://localhost/uploads/1.csv' },
1761
+ })
1762
+ assert.equal(model.created.length, 1, 'only Grace makes it through')
1763
+ const notify = (r as { notify?: { type: string; title: string; body?: string } }).notify
1764
+ assert.equal(notify?.type, 'warning')
1765
+ assert.match(notify!.title, /1 created/)
1766
+ assert.match(notify!.title, /1 skipped/)
1767
+ assert.match(notify!.body ?? '', /Row 1: ada is reserved/)
1768
+ })
1769
+ })
1770
+
1771
+ it('thrown error during create → row counted as skipped + surfaced in notification body', async () => {
1772
+ const failing = {
1773
+ created: [] as Record<string, unknown>[],
1774
+ async create() { throw new Error('db down') },
1775
+ query() { return { where() { return { async paginate() { return { data: [] } } } } } },
1776
+ }
1777
+ const R = makeR({ model: failing as unknown as ReturnType<typeof makeModel> })
1778
+ const a = Action.import(R, '/admin')
1779
+ await withFetch('name\r\nAda\r\n', async () => {
1780
+ const r = await a.getHandler()!({
1781
+ values: { file: 'http://localhost/uploads/1.csv' },
1782
+ })
1783
+ const notify = (r as { notify?: { type: string; body?: string } }).notify
1784
+ assert.equal(notify?.type, 'warning')
1785
+ assert.match(notify!.body ?? '', /Row 1: db down/)
1786
+ })
1787
+ })
1788
+
1789
+ it('opts.columns remap CSV headers to model attribute keys', async () => {
1790
+ const model = makeModel()
1791
+ const R = makeR({ model })
1792
+ const a = Action.import(R, '/admin', {
1793
+ columns: { 'Full Name': 'name', 'Email Address': 'email' },
1794
+ })
1795
+ await withFetch('Full Name,Email Address\r\nAda,ada@x\r\n', async () => {
1796
+ await a.getHandler()!({
1797
+ values: { file: 'http://localhost/uploads/1.csv' },
1798
+ })
1799
+ assert.deepEqual(model.created[0], { name: 'Ada', email: 'ada@x' })
1800
+ })
1801
+ })
1802
+
1803
+ it('beforeCreate hook can mutate the row payload', async () => {
1804
+ const model = makeModel()
1805
+ const R = makeR({ model })
1806
+ const a = Action.import(R, '/admin', {
1807
+ beforeCreate: (row) => ({ ...row, source: 'csv-import' }),
1808
+ })
1809
+ await withFetch('name\r\nAda\r\n', async () => {
1810
+ await a.getHandler()!({
1811
+ values: { file: 'http://localhost/uploads/1.csv' },
1812
+ })
1813
+ assert.deepEqual(model.created[0], { name: 'Ada', source: 'csv-import' })
1814
+ })
1815
+ })
1816
+
1817
+ it('emits an error notification when no file is uploaded', async () => {
1818
+ const a = Action.import(makeR(), '/admin')
1819
+ const r = await a.getHandler()!({ values: {} })
1820
+ const notify = (r as { notify?: { type: string; title: string } }).notify
1821
+ assert.equal(notify?.type, 'error')
1822
+ assert.match(notify!.title, /No file uploaded/)
1823
+ })
1824
+
1825
+ it('emits an error notification when R.model.create is missing', async () => {
1826
+ const R = { labelSingular: 'X', getSlug: () => 'x', model: {} }
1827
+ const a = Action.import(R, '/admin')
1828
+ const r = await a.getHandler()!({ values: { file: 'http://localhost/uploads/1.csv' } })
1829
+ const notify = (r as { notify?: { type: string; title: string } }).notify
1830
+ assert.equal(notify?.type, 'error')
1831
+ assert.match(notify!.title, /no model.create/)
1832
+ })
1833
+
1834
+ it('emits an error notification when row count exceeds maxRows', async () => {
1835
+ const model = makeModel()
1836
+ const R = makeR({ model })
1837
+ const a = Action.import(R, '/admin', { maxRows: 1 })
1838
+ await withFetch('name\r\nA\r\nB\r\n', async () => {
1839
+ const r = await a.getHandler()!({
1840
+ values: { file: 'http://localhost/uploads/1.csv' },
1841
+ })
1842
+ const notify = (r as { notify?: { type: string; title: string } }).notify
1843
+ assert.equal(notify?.type, 'error')
1844
+ assert.match(notify!.title, /too large \(2 > 1\)/)
1845
+ assert.equal(model.created.length, 0, 'no rows written when cap exceeded')
1846
+ })
1847
+ })
1848
+
1849
+ it('JSON format imports parse arrays and single objects', async () => {
1850
+ const model = makeModel()
1851
+ const R = makeR({ model })
1852
+ const a = Action.import(R, '/admin', { format: 'json' })
1853
+ await withFetch('[{"name":"Ada"},{"name":"Grace"}]', async () => {
1854
+ await a.getHandler()!({
1855
+ values: { file: 'http://localhost/uploads/1.json' },
1856
+ })
1857
+ assert.equal(model.created.length, 2)
1858
+ })
1859
+ })
1860
+
1861
+ it('format auto-detects from filename extension when not set', async () => {
1862
+ const model = makeModel()
1863
+ const R = makeR({ model })
1864
+ const a = Action.import(R, '/admin')
1865
+ await withFetch('{"name":"Ada"}', async () => {
1866
+ await a.getHandler()!({
1867
+ values: { file: 'http://localhost/uploads/x.json' },
1868
+ })
1869
+ assert.equal(model.created.length, 1)
1870
+ assert.equal(model.created[0]!['name'], 'Ada')
1871
+ })
1872
+ })
1873
+
1874
+ it('onComplete hook fires once after the import loop with the summary', async () => {
1875
+ const seen: unknown[] = []
1876
+ const a = Action.import(makeR(), '/admin', {
1877
+ onComplete: (s) => { seen.push(s) },
1878
+ })
1879
+ await withFetch('name\r\nA\r\nB\r\n', async () => {
1880
+ await a.getHandler()!({
1881
+ values: { file: 'http://localhost/uploads/1.csv' },
1882
+ })
1883
+ })
1884
+ assert.equal(seen.length, 1)
1885
+ assert.equal((seen[0] as { created: number }).created, 2)
1886
+ })
1887
+
1888
+ it('visibility delegates to R.canCreate — denials hide the action', async () => {
1889
+ const a = Action.import(makeR({ canCreate: async () => false }), '/admin')
1890
+ const ev = await a.evaluate({ user: { id: 'u' } })
1891
+ assert.equal(ev.visible, false)
1892
+ })
1893
+ })
1894
+
1895
+ describe('Action.relationReplicate / relationBulkReplicate', () => {
1896
+ // Parent model — exposes `static relations` so the FK descriptor
1897
+ // resolves through `getParentRelationDescriptor`.
1898
+ class UserModel {
1899
+ id?: string
1900
+ static primaryKey = 'id'
1901
+ static relations = {
1902
+ posts: { type: 'hasMany', foreignKey: 'userId', model: () => PostModel },
1903
+ comments: { type: 'morphMany', morphName: 'commentable', model: () => CommentModel },
1904
+ }
1905
+ constructor(over: Partial<UserModel> = {}) { Object.assign(this, over) }
1906
+ }
1907
+
1908
+ // Child models. Test stubs that record every `create()` call's input
1909
+ // payload (NOT the returned row) so assertions on stripping land on
1910
+ // what the factory actually passed in.
1911
+ class PostModel {
1912
+ static primaryKey = 'id'
1913
+ static created: Array<Record<string, unknown>> = []
1914
+ static throwOnNextCreate = false
1915
+ static async create(data: Record<string, unknown>) {
1916
+ if (PostModel.throwOnNextCreate) {
1917
+ PostModel.throwOnNextCreate = false
1918
+ throw new Error('boom')
1919
+ }
1920
+ PostModel.created.push({ ...data })
1921
+ return { id: String(PostModel.created.length), ...data }
1922
+ }
1923
+ }
1924
+
1925
+ class CommentModel {
1926
+ static primaryKey = 'id'
1927
+ static created: Array<Record<string, unknown>> = []
1928
+ static async create(data: Record<string, unknown>) {
1929
+ CommentModel.created.push({ ...data })
1930
+ return { id: String(CommentModel.created.length), ...data }
1931
+ }
1932
+ }
1933
+
1934
+ class Posts extends RelationManager {
1935
+ static override relationship = 'posts'
1936
+ static override label = 'Posts'
1937
+ static override labelSingular = 'Post'
1938
+ }
1939
+
1940
+ class Comments extends RelationManager {
1941
+ static override relationship = 'comments'
1942
+ static override label = 'Comments'
1943
+ static override labelSingular = 'Comment'
1944
+ }
1945
+
1946
+ function freshHasManyCtx(): RelationManagerContext {
1947
+ PostModel.created = []
1948
+ PostModel.throwOnNextCreate = false
1949
+ const Related = {
1950
+ labelSingular: 'Post',
1951
+ label: 'Posts',
1952
+ getSlug: () => 'posts',
1953
+ model: PostModel,
1954
+ deletedAtColumn: 'deletedAt',
1955
+ } as unknown as RelationManagerContext['related']
1956
+ return {
1957
+ basePath: '/admin',
1958
+ parentSlug: 'users',
1959
+ parentId: '42',
1960
+ relationship: 'posts',
1961
+ parentRecord: new UserModel({ id: '42' }),
1962
+ related: Related,
1963
+ mode: 'hasMany',
1964
+ }
1965
+ }
1966
+
1967
+ function freshMorphManyCtx(): RelationManagerContext {
1968
+ CommentModel.created = []
1969
+ const Related = {
1970
+ labelSingular: 'Comment',
1971
+ label: 'Comments',
1972
+ getSlug: () => 'comments',
1973
+ model: CommentModel,
1974
+ deletedAtColumn: 'deletedAt',
1975
+ } as unknown as RelationManagerContext['related']
1976
+ return {
1977
+ basePath: '/admin',
1978
+ parentSlug: 'users',
1979
+ parentId: '42',
1980
+ relationship: 'comments',
1981
+ parentRecord: new UserModel({ id: '42' }),
1982
+ related: Related,
1983
+ mode: 'morphMany',
1984
+ }
1985
+ }
1986
+
1987
+ describe('toMeta + placement', () => {
1988
+ it('relationReplicate is a row-placement handler action', () => {
1989
+ const meta = Action.relationReplicate(Posts, freshHasManyCtx()).toMeta()
1990
+ assert.equal(meta.placement, 'row')
1991
+ assert.equal(meta.label, 'Replicate')
1992
+ assert.equal(meta.method, undefined) // handler-style, no form-post
1993
+ assert.equal(meta.href, undefined)
1994
+ })
1995
+
1996
+ it('relationBulkReplicate is a bulk-placement handler action with confirm', () => {
1997
+ const meta = Action.relationBulkReplicate(Posts, freshHasManyCtx()).toMeta()
1998
+ assert.equal(meta.placement, 'bulk')
1999
+ assert.match(meta.confirm?.message ?? '', /Replicate the selected/)
2000
+ })
2001
+ })
2002
+
2003
+ describe('hasMany — clone preserves FK by force-pin', () => {
2004
+ it('strips PK + soft-delete + excludeAttributes and re-pins userId from the parent', async () => {
2005
+ const ctx = freshHasManyCtx()
2006
+ const handler = Action.relationReplicate(Posts, ctx, undefined, {
2007
+ excludeAttributes: ['slug'],
2008
+ }).getHandler()!
2009
+ await handler({
2010
+ record: { id: '7', title: 'Hello', body: 'World', slug: 'hello', userId: '99', deletedAt: null },
2011
+ user: null,
2012
+ })
2013
+ assert.equal(PostModel.created.length, 1)
2014
+ const replica = PostModel.created[0]!
2015
+ assert.equal(replica['id'], undefined, 'PK stripped')
2016
+ assert.equal(replica['deletedAt'], undefined, 'soft-delete column stripped')
2017
+ assert.equal(replica['slug'], undefined, 'excludeAttributes honored')
2018
+ assert.equal(replica['title'], 'Hello')
2019
+ assert.equal(replica['body'], 'World')
2020
+ // The source row carried `userId: '99'` (a tampered value or
2021
+ // a row from a different parent's children). The factory MUST
2022
+ // overwrite it with the manager's parentId so the replica
2023
+ // stays attached to the right parent.
2024
+ assert.equal(replica['userId'], '42', 'FK re-pinned to ctx.parentId')
2025
+ })
2026
+
2027
+ it('beforeReplicaSaved runs after the FK pin and can mutate non-FK fields', async () => {
2028
+ const ctx = freshHasManyCtx()
2029
+ const handler = Action.relationReplicate(Posts, ctx, undefined, {
2030
+ beforeReplicaSaved: (replica) => ({ ...replica, title: `Copy of ${replica['title']}` }),
2031
+ }).getHandler()!
2032
+ await handler({ record: { id: '1', title: 'Hello', userId: '42' }, user: null })
2033
+ assert.equal(PostModel.created[0]!['title'], 'Copy of Hello')
2034
+ assert.equal(PostModel.created[0]!['userId'], '42')
2035
+ })
2036
+
2037
+ it('returns a success notify with the manager singular label', async () => {
2038
+ const ctx = freshHasManyCtx()
2039
+ const handler = Action.relationReplicate(Posts, ctx).getHandler()!
2040
+ const result = await handler({ record: { id: '1', title: 'Hello' }, user: null })
2041
+ const r = result as { notify: { title: string; type: string } }
2042
+ assert.match(r.notify.title, /^Post replicated$/)
2043
+ assert.equal(r.notify.type, 'success')
2044
+ })
2045
+
2046
+ it('catches Related.model.create errors and surfaces an error notify', async () => {
2047
+ const ctx = freshHasManyCtx()
2048
+ PostModel.throwOnNextCreate = true
2049
+ const handler = Action.relationReplicate(Posts, ctx).getHandler()!
2050
+ const result = await handler({ record: { id: '1', title: 'X' }, user: null })
2051
+ const notify = (result as { notify: { title: string; type: string } }).notify
2052
+ assert.match(notify.title, /^Replicate failed: boom$/)
2053
+ assert.equal(notify.type, 'error')
2054
+ })
2055
+ })
2056
+
2057
+ describe('morphMany — clone re-stamps the morph payload', () => {
2058
+ it('overwrites <morphName>Id + <morphName>Type from the parent record', async () => {
2059
+ const ctx = freshMorphManyCtx()
2060
+ const handler = Action.relationReplicate(Comments, ctx).getHandler()!
2061
+ await handler({
2062
+ record: { id: '7', body: 'Nice!', commentableId: 'WRONG', commentableType: 'WRONG' },
2063
+ user: null,
2064
+ })
2065
+ const replica = CommentModel.created[0]!
2066
+ assert.equal(replica['id'], undefined, 'PK stripped')
2067
+ assert.equal(replica['body'], 'Nice!')
2068
+ assert.equal(replica['commentableId'], '42', 'morph id re-stamped from parent')
2069
+ assert.equal(replica['commentableType'], 'UserModel', 'morph type re-stamped from parent constructor')
2070
+ })
2071
+ })
2072
+
2073
+ describe('visibility', () => {
2074
+ it('hidden under belongsToMany mode', async () => {
2075
+ const ctx = { ...freshHasManyCtx(), mode: 'belongsToMany' as const }
2076
+ const ev = await Action.relationReplicate(Posts, ctx).evaluate({})
2077
+ assert.equal(ev.visible, false)
2078
+ })
2079
+
2080
+ it('hidden under morphTo mode (no single owner to pin to)', async () => {
2081
+ const ctx = { ...freshHasManyCtx(), mode: 'morphTo' as const }
2082
+ const ev = await Action.relationReplicate(Posts, ctx).evaluate({})
2083
+ assert.equal(ev.visible, false)
2084
+ })
2085
+
2086
+ it('delegates to manager.canCreate when overridden', async () => {
2087
+ class Forbidden extends RelationManager {
2088
+ static override relationship = 'posts'
2089
+ static override async canCreate(): Promise<boolean> { return false }
2090
+ }
2091
+ const ev = await Action.relationReplicate(Forbidden, freshHasManyCtx()).evaluate({})
2092
+ assert.equal(ev.visible, false)
2093
+ })
2094
+
2095
+ it('falls through to related Resource canCreate when manager unset', async () => {
2096
+ const ctx = freshHasManyCtx()
2097
+ ;(ctx.related as unknown as { canCreate: () => Promise<boolean> }).canCreate = async () => false
2098
+ const ev = await Action.relationReplicate(Posts, ctx).evaluate({})
2099
+ assert.equal(ev.visible, false)
2100
+ })
2101
+
2102
+ it('allows when neither manager nor related Resource opts in', async () => {
2103
+ const ev = await Action.relationReplicate(Posts, freshHasManyCtx()).evaluate({})
2104
+ assert.equal(ev.visible, true)
2105
+ })
2106
+ })
2107
+
2108
+ describe('error paths', () => {
2109
+ it('returns an error notify when ctx.record is missing', async () => {
2110
+ const handler = Action.relationReplicate(Posts, freshHasManyCtx()).getHandler()!
2111
+ const result = await handler({ user: null })
2112
+ const notify = (result as { notify: { title: string; type: string } }).notify
2113
+ assert.match(notify.title, /source record missing/i)
2114
+ })
2115
+
2116
+ it('returns an error notify when Related.model is missing', async () => {
2117
+ const ctx: RelationManagerContext = { ...freshHasManyCtx(), related: { } as unknown as RelationManagerContext['related'] }
2118
+ const handler = Action.relationReplicate(Posts, ctx).getHandler()!
2119
+ const result = await handler({ record: { id: '1' }, user: null })
2120
+ const notify = (result as { notify: { title: string; type: string } }).notify
2121
+ assert.match(notify.title, /not configured/i)
2122
+ })
2123
+ })
2124
+
2125
+ describe('bulk', () => {
2126
+ it('iterates ctx.records — one create per source, FK re-pinned each time', async () => {
2127
+ const ctx = freshHasManyCtx()
2128
+ const handler = Action.relationBulkReplicate(Posts, ctx).getHandler()!
2129
+ const result = await handler({
2130
+ records: [
2131
+ { id: '1', title: 'A', userId: 'wrong' },
2132
+ { id: '2', title: 'B', userId: 'wrong' },
2133
+ { id: '3', title: 'C' },
2134
+ ],
2135
+ user: null,
2136
+ })
2137
+ assert.equal(PostModel.created.length, 3)
2138
+ assert.deepEqual(PostModel.created.map(r => r['title']), ['A', 'B', 'C'])
2139
+ assert.deepEqual(PostModel.created.map(r => r['userId']), ['42', '42', '42'])
2140
+ assert.match((result as { notify: { title: string } }).notify.title, /^3 posts replicated$/)
2141
+ })
2142
+
2143
+ it('skips rows where create throws and reports only successful count', async () => {
2144
+ const ctx = freshHasManyCtx()
2145
+ // Fail the second create.
2146
+ let calls = 0
2147
+ const orig = PostModel.create.bind(PostModel)
2148
+ PostModel.create = async (d) => {
2149
+ calls++
2150
+ if (calls === 2) throw new Error('boom')
2151
+ return orig(d)
2152
+ }
2153
+ try {
2154
+ const handler = Action.relationBulkReplicate(Posts, ctx).getHandler()!
2155
+ const result = await handler({
2156
+ records: [{ id: '1' }, { id: '2' }, { id: '3' }],
2157
+ user: null,
2158
+ })
2159
+ assert.match((result as { notify: { title: string } }).notify.title, /^2 posts replicated$/)
2160
+ } finally {
2161
+ PostModel.create = orig
2162
+ }
2163
+ })
2164
+
2165
+ it('skips rows where per-row policy denies', async () => {
2166
+ const ctx = freshHasManyCtx()
2167
+ let i = 0
2168
+ class GatedPosts extends RelationManager {
2169
+ static override relationship = 'posts'
2170
+ static override label = 'Posts'
2171
+ static override labelSingular = 'Post'
2172
+ static override async canCreate(): Promise<boolean> {
2173
+ i++
2174
+ return i !== 2
2175
+ }
2176
+ }
2177
+ const handler = Action.relationBulkReplicate(GatedPosts, ctx).getHandler()!
2178
+ const result = await handler({
2179
+ records: [{ id: '1' }, { id: '2' }, { id: '3' }],
2180
+ user: null,
2181
+ })
2182
+ assert.equal(PostModel.created.length, 2)
2183
+ assert.match((result as { notify: { title: string } }).notify.title, /^2 posts replicated$/)
2184
+ })
2185
+
2186
+ it('singularises the count copy when exactly one row succeeds', async () => {
2187
+ const ctx = freshHasManyCtx()
2188
+ const handler = Action.relationBulkReplicate(Posts, ctx).getHandler()!
2189
+ const result = await handler({
2190
+ records: [{ id: '1', title: 'Solo' }],
2191
+ user: null,
2192
+ })
2193
+ assert.match((result as { notify: { title: string } }).notify.title, /^1 post replicated$/)
2194
+ })
2195
+
2196
+ it('returns an error notify when Related.model.create is missing', async () => {
2197
+ const ctx: RelationManagerContext = { ...freshHasManyCtx(), related: { } as unknown as RelationManagerContext['related'] }
2198
+ const handler = Action.relationBulkReplicate(Posts, ctx).getHandler()!
2199
+ const result = await handler({ records: [{ id: '1' }], user: null })
2200
+ const notify = (result as { notify: { title: string; type: string } }).notify
2201
+ assert.match(notify.title, /not configured/i)
2202
+ assert.equal(notify.type, 'error')
2203
+ })
2204
+
2205
+ it('hidden under M2M / morphTo mode', async () => {
2206
+ const m2m = await Action.relationBulkReplicate(Posts, { ...freshHasManyCtx(), mode: 'belongsToMany' }).evaluate({})
2207
+ assert.equal(m2m.visible, false)
2208
+ const mt = await Action.relationBulkReplicate(Posts, { ...freshHasManyCtx(), mode: 'morphTo' }).evaluate({})
2209
+ assert.equal(mt.visible, false)
2210
+ })
2211
+ })
2212
+
2213
+ describe('opts.getCreatedNotificationTitle / getRedirectUrl overrides', () => {
2214
+ it('relationReplicate honors getCreatedNotificationTitle with replica + source', async () => {
2215
+ const ctx = freshHasManyCtx()
2216
+ let seenReplica: unknown
2217
+ let seenSource: unknown
2218
+ const handler = Action.relationReplicate(Posts, ctx, undefined, {
2219
+ getCreatedNotificationTitle: ({ replica, source }) => {
2220
+ seenReplica = replica
2221
+ seenSource = source
2222
+ return `Cloned post for user ${(replica as { userId: string }).userId}`
2223
+ },
2224
+ }).getHandler()!
2225
+ const result = await handler({ record: { id: '7', title: 'A' }, user: null })
2226
+ assert.equal((result as { notify: { title: string } }).notify.title, 'Cloned post for user 42')
2227
+ assert.deepEqual(seenSource, { id: '7', title: 'A' })
2228
+ // Replica is the model.create result (id stamped by the stub).
2229
+ assert.equal((seenReplica as { userId: string })?.userId, '42')
2230
+ })
2231
+
2232
+ it('relationReplicate honors getRedirectUrl — emits result.redirect', async () => {
2233
+ const ctx = freshHasManyCtx()
2234
+ const handler = Action.relationReplicate(Posts, ctx, undefined, {
2235
+ getRedirectUrl: ({ replica }) => `/admin/users/42/posts/${(replica as { id: string }).id}/preview`,
2236
+ }).getHandler()!
2237
+ const result = await handler({ record: { id: '7', title: 'A' }, user: null })
2238
+ assert.equal((result as { redirect: string }).redirect, '/admin/users/42/posts/1/preview')
2239
+ })
2240
+
2241
+ it('relationReplicate without getRedirectUrl emits no redirect (route fallback)', async () => {
2242
+ const ctx = freshHasManyCtx()
2243
+ const handler = Action.relationReplicate(Posts, ctx).getHandler()!
2244
+ const result = await handler({ record: { id: '7', title: 'A' }, user: null })
2245
+ // Default behavior: handler doesn't set redirect; the route layer
2246
+ // owns the fallback to the manager list URL. Asserting redirect
2247
+ // is absent (not empty string).
2248
+ assert.equal((result as { redirect?: string }).redirect, undefined)
2249
+ })
2250
+
2251
+ it('relationReplicate getCreatedNotificationTitle returning undefined falls back', async () => {
2252
+ const ctx = freshHasManyCtx()
2253
+ const handler = Action.relationReplicate(Posts, ctx, undefined, {
2254
+ getCreatedNotificationTitle: () => undefined,
2255
+ }).getHandler()!
2256
+ const result = await handler({ record: { id: '7', title: 'A' }, user: null })
2257
+ assert.match((result as { notify: { title: string } }).notify.title, /^Post replicated$/)
2258
+ })
2259
+
2260
+ it('relationBulkReplicate honors getCreatedNotificationTitle with count + records', async () => {
2261
+ const ctx = freshHasManyCtx()
2262
+ let seenCount: unknown
2263
+ const handler = Action.relationBulkReplicate(Posts, ctx, {
2264
+ getCreatedNotificationTitle: ({ count, records }) => {
2265
+ seenCount = count
2266
+ return `Duplicated ${count} of ${(records as unknown[]).length} into user 42`
2267
+ },
2268
+ }).getHandler()!
2269
+ const result = await handler({
2270
+ records: [{ id: '1' }, { id: '2' }],
2271
+ user: null,
2272
+ })
2273
+ assert.equal((result as { notify: { title: string } }).notify.title, 'Duplicated 2 of 2 into user 42')
2274
+ assert.equal(seenCount, 2)
2275
+ })
2276
+
2277
+ it('overrides may be async', async () => {
2278
+ const ctx = freshHasManyCtx()
2279
+ const handler = Action.relationReplicate(Posts, ctx, undefined, {
2280
+ getCreatedNotificationTitle: async () => 'async title',
2281
+ getRedirectUrl: async () => '/admin/elsewhere',
2282
+ }).getHandler()!
2283
+ const result = await handler({ record: { id: '1', title: 'X' }, user: null }) as {
2284
+ redirect: string
2285
+ notify: { title: string }
2286
+ }
2287
+ assert.equal(result.notify.title, 'async title')
2288
+ assert.equal(result.redirect, '/admin/elsewhere')
2289
+ })
2290
+ })
2291
+ })
2292
+
2293
+ describe('Action.markAsRead factory', () => {
2294
+ it('builds a method-POST action targeting the read endpoint', () => {
2295
+ const meta = Action.markAsRead('/admin', 'n-7').toMeta()
2296
+ assert.equal(meta.method, 'post')
2297
+ assert.equal(meta.action, '/admin/_notifications/n-7/read')
2298
+ assert.equal(meta.label, 'Mark as read')
2299
+ assert.equal(meta.name, 'markAsRead')
2300
+ })
2301
+
2302
+ it('defaults the id to :id template for row context', () => {
2303
+ const meta = Action.markAsRead('/admin').toMeta()
2304
+ assert.equal(meta.action, '/admin/_notifications/:id/read')
2305
+ })
2306
+
2307
+ it('honors a non-default base path', () => {
2308
+ const meta = Action.markAsRead('/dashboard', 'abc').toMeta()
2309
+ assert.equal(meta.action, '/dashboard/_notifications/abc/read')
2310
+ })
2311
+
2312
+ it('has no built-in visibility — always shows by default', async () => {
2313
+ const a = Action.markAsRead('/admin', 'n-7')
2314
+ const r = await a.evaluate({ record: { id: 'n-7' } })
2315
+ assert.equal(r.visible, true)
2316
+ })
2317
+
2318
+ it('composes with .visible(...) to hide already-read rows', async () => {
2319
+ const a = Action.markAsRead('/admin', ':id')
2320
+ .visible(({ record }) => !(record as { readAt?: string | null })?.readAt)
2321
+ const unread = await a.evaluate({ record: { id: 'n-7', readAt: null } })
2322
+ const read = await a.evaluate({ record: { id: 'n-8', readAt: '2026-05-07T12:00:00Z' } })
2323
+ assert.equal(unread.visible, true)
2324
+ assert.equal(read.visible, false)
2325
+ })
2326
+ })
2327
+
2328
+ describe('Action modal chrome extras (audit gap #2)', () => {
2329
+ // Helper: pull the `modal` slot off a built meta. Cast through unknown
2330
+ // because `ActionMeta.modal` is sparse — the tests want a concrete map.
2331
+ type ModalSlot = {
2332
+ closeByClickingAway?: boolean
2333
+ closeByEscaping?: boolean
2334
+ stickyHeader?: boolean
2335
+ stickyFooter?: boolean
2336
+ autofocus?: boolean
2337
+ alignment?: 'start'|'center'|'end'
2338
+ iconColor?: string
2339
+ closeButton?: boolean
2340
+ submitLabel?: string
2341
+ cancelLabel?: string
2342
+ }
2343
+ const modal = (a: Action): ModalSlot | undefined =>
2344
+ (a.toMeta() as unknown as { modal?: ModalSlot }).modal
2345
+
2346
+ it('emits no modal slot when no modal setter ran', () => {
2347
+ assert.equal(modal(Action.make('save')), undefined)
2348
+ })
2349
+
2350
+ describe('closeModalByClickingAway / closeModalByEscaping', () => {
2351
+ it('default (true) does NOT emit either flag', () => {
2352
+ // Force a modal slot via .modalHeading so the rest of the meta builds,
2353
+ // but don't touch the close-* flags. Defaults must round-trip as
2354
+ // omissions (sparse meta) so existing modals stay byte-identical.
2355
+ const a = Action.make('a').modalHeading('Hello')
2356
+ const m = modal(a)
2357
+ assert.equal(m?.closeByClickingAway, undefined)
2358
+ assert.equal(m?.closeByEscaping, undefined)
2359
+ })
2360
+
2361
+ it('closeModalByClickingAway() with no arg disables (Filament shape)', () => {
2362
+ const m = modal(Action.make('a').closeModalByClickingAway())
2363
+ assert.equal(m?.closeByClickingAway, false)
2364
+ })
2365
+
2366
+ it('closeModalByEscaping(false) disables; closeModalByEscaping(true) re-arms', () => {
2367
+ assert.equal(modal(Action.make('a').closeModalByEscaping(false))?.closeByEscaping, false)
2368
+ // Re-arming back to default must not emit (sparse).
2369
+ assert.equal(modal(Action.make('a').closeModalByEscaping(false).closeModalByEscaping(true))?.closeByEscaping, undefined)
2370
+ })
2371
+ })
2372
+
2373
+ describe('sticky chrome', () => {
2374
+ it('stickyModalHeader() arms by default; stickyModalHeader(false) disarms', () => {
2375
+ assert.equal(modal(Action.make('a').stickyModalHeader())?.stickyHeader, true)
2376
+ assert.equal(modal(Action.make('a').stickyModalHeader(false))?.stickyHeader, undefined)
2377
+ })
2378
+
2379
+ it('stickyModalFooter() round-trips independently', () => {
2380
+ const m = modal(Action.make('a').stickyModalFooter())
2381
+ assert.equal(m?.stickyHeader, undefined)
2382
+ assert.equal(m?.stickyFooter, true)
2383
+ })
2384
+ })
2385
+
2386
+ describe('modalAutofocus', () => {
2387
+ it('omits when not set', () => {
2388
+ assert.equal(modal(Action.make('a').modalHeading('h'))?.autofocus, undefined)
2389
+ })
2390
+
2391
+ it('emits true when called with no arg', () => {
2392
+ assert.equal(modal(Action.make('a').modalAutofocus())?.autofocus, true)
2393
+ })
2394
+
2395
+ it('emits false when explicitly disarmed', () => {
2396
+ assert.equal(modal(Action.make('a').modalAutofocus(false))?.autofocus, false)
2397
+ })
2398
+ })
2399
+
2400
+ describe('modalAlignment + modalIconColor', () => {
2401
+ it('emits the alignment string', () => {
2402
+ assert.equal(modal(Action.make('a').modalAlignment('start'))?.alignment, 'start')
2403
+ assert.equal(modal(Action.make('a').modalAlignment('end'))?.alignment, 'end')
2404
+ })
2405
+
2406
+ it('emits the icon color string', () => {
2407
+ assert.equal(modal(Action.make('a').modalIconColor('warning'))?.iconColor, 'warning')
2408
+ })
2409
+ })
2410
+
2411
+ describe('modalCloseButton', () => {
2412
+ it('default (off) emits no flag', () => {
2413
+ assert.equal(modal(Action.make('a').modalHeading('h'))?.closeButton, undefined)
2414
+ })
2415
+
2416
+ it('modalCloseButton() arms; modalCloseButton(false) disarms', () => {
2417
+ assert.equal(modal(Action.make('a').modalCloseButton())?.closeButton, true)
2418
+ assert.equal(modal(Action.make('a').modalCloseButton(false))?.closeButton, undefined)
2419
+ })
2420
+ })
2421
+
2422
+ describe('Filament v5 alias setters', () => {
2423
+ it('modalSubmitActionLabel routes to modalSubmitLabel', () => {
2424
+ const m = modal(Action.make('a').modalSubmitActionLabel('Apply'))
2425
+ assert.equal(m?.submitLabel, 'Apply')
2426
+ })
2427
+
2428
+ it('modalCancelActionLabel routes to modalCancelLabel', () => {
2429
+ const m = modal(Action.make('a').modalCancelActionLabel('Never mind'))
2430
+ assert.equal(m?.cancelLabel, 'Never mind')
2431
+ })
2432
+ })
2433
+
2434
+ it('any modal setter flips _hasModal (modal slot present)', () => {
2435
+ // Each setter must trigger `_hasModal = true` so the slot gets emitted
2436
+ // even when the user only customizes one chrome detail (no schema, no
2437
+ // heading). Otherwise `closeModalByClickingAway()` on a confirm-only
2438
+ // action would silently drop on the floor.
2439
+ for (const customise of [
2440
+ (a: Action) => a.closeModalByClickingAway(),
2441
+ (a: Action) => a.closeModalByEscaping(false),
2442
+ (a: Action) => a.stickyModalHeader(),
2443
+ (a: Action) => a.stickyModalFooter(),
2444
+ (a: Action) => a.modalAutofocus(),
2445
+ (a: Action) => a.modalAlignment('end'),
2446
+ (a: Action) => a.modalIconColor('info'),
2447
+ (a: Action) => a.modalCloseButton(),
2448
+ ]) {
2449
+ const a = customise(Action.make('a'))
2450
+ assert.notEqual(modal(a), undefined, 'modal slot must be present')
2451
+ }
2452
+ })
2453
+
2454
+ it('chains across multiple chrome setters in one builder', () => {
2455
+ const m = modal(
2456
+ Action.make('save')
2457
+ .modalHeading('Bulk update')
2458
+ .modalAlignment('end')
2459
+ .modalIconColor('primary')
2460
+ .stickyModalHeader()
2461
+ .stickyModalFooter()
2462
+ .modalAutofocus(false)
2463
+ .closeModalByClickingAway()
2464
+ .closeModalByEscaping(false)
2465
+ .modalCloseButton(),
2466
+ )
2467
+ assert.deepEqual(m, {
2468
+ heading: 'Bulk update',
2469
+ alignment: 'end',
2470
+ iconColor: 'primary',
2471
+ stickyHeader: true,
2472
+ stickyFooter: true,
2473
+ autofocus: false,
2474
+ closeByClickingAway: false,
2475
+ closeByEscaping: false,
2476
+ closeButton: true,
2477
+ })
2478
+ })
2479
+ })