@antelopejs/interface-dms 0.10.3 → 0.12.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 (377) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +21 -10
  3. package/dist/attachments.d.ts +15 -2
  4. package/dist/attachments.js +14 -1
  5. package/dist/attachments.js.map +1 -1
  6. package/dist/auth/db/models/sessions.model.d.ts +7 -5
  7. package/dist/auth/db/models/sessions.model.js +2 -9
  8. package/dist/auth/db/models/sessions.model.js.map +1 -1
  9. package/dist/auth/db/models/userExternalIdentities.model.d.ts +7 -5
  10. package/dist/auth/db/models/users.model.d.ts +7 -5
  11. package/dist/auth/db/tables/users.table.d.ts +4 -2
  12. package/dist/auth/db/tables/users.table.js.map +1 -1
  13. package/dist/auth/index.d.ts +4 -4
  14. package/dist/auth/internal/jwt-auth-decorator.d.ts +1 -1
  15. package/dist/auth/internal/jwt-auth-decorator.js +6 -3
  16. package/dist/auth/internal/jwt-auth-decorator.js.map +1 -1
  17. package/dist/auth/internal/request-authenticators.d.ts +12 -0
  18. package/dist/auth/internal/request-authenticators.js +53 -10
  19. package/dist/auth/internal/request-authenticators.js.map +1 -1
  20. package/dist/base/activity-feed.d.ts +34 -32
  21. package/dist/base/activity-feed.js +2 -0
  22. package/dist/base/activity-feed.js.map +1 -1
  23. package/dist/base/banner.d.ts +12 -10
  24. package/dist/base/banner.js +2 -0
  25. package/dist/base/banner.js.map +1 -1
  26. package/dist/base/block-registry/internal/describe.js +3 -9
  27. package/dist/base/block-registry/internal/describe.js.map +1 -1
  28. package/dist/base/block-registry/internal/ui.js +1 -1
  29. package/dist/base/block-registry/internal/ui.js.map +1 -1
  30. package/dist/base/card.d.ts +11 -9
  31. package/dist/base/card.js +2 -0
  32. package/dist/base/card.js.map +1 -1
  33. package/dist/base/chart-card.d.ts +4 -2
  34. package/dist/base/chart-card.js +2 -0
  35. package/dist/base/chart-card.js.map +1 -1
  36. package/dist/base/chart-schemas.d.ts +148 -148
  37. package/dist/base/chart.d.ts +65 -0
  38. package/dist/base/chart.js +65 -0
  39. package/dist/base/chart.js.map +1 -1
  40. package/dist/base/code-block.d.ts +5 -3
  41. package/dist/base/code-block.js +2 -0
  42. package/dist/base/code-block.js.map +1 -1
  43. package/dist/base/custom.d.ts +5 -0
  44. package/dist/base/custom.js +5 -0
  45. package/dist/base/custom.js.map +1 -1
  46. package/dist/base/data-types/default/address.d.ts +5 -0
  47. package/dist/base/data-types/default/address.js +5 -0
  48. package/dist/base/data-types/default/address.js.map +1 -1
  49. package/dist/base/data-types/default/choices.d.ts +20 -0
  50. package/dist/base/data-types/default/choices.js +20 -0
  51. package/dist/base/data-types/default/choices.js.map +1 -1
  52. package/dist/base/data-types/default/files.d.ts +22 -5
  53. package/dist/base/data-types/default/files.js +20 -0
  54. package/dist/base/data-types/default/files.js.map +1 -1
  55. package/dist/base/data-types/default/numbers.d.ts +15 -0
  56. package/dist/base/data-types/default/numbers.js +25 -9
  57. package/dist/base/data-types/default/numbers.js.map +1 -1
  58. package/dist/base/data-types/default/relations.d.ts +10 -2
  59. package/dist/base/data-types/default/relations.js +9 -6
  60. package/dist/base/data-types/default/relations.js.map +1 -1
  61. package/dist/base/data-types/default/strings.d.ts +25 -0
  62. package/dist/base/data-types/default/strings.js +25 -0
  63. package/dist/base/data-types/default/strings.js.map +1 -1
  64. package/dist/base/data-types/default-types.d.ts +1 -1
  65. package/dist/base/data-types/field-types.d.ts +10 -0
  66. package/dist/base/data-types/field-types.js +10 -0
  67. package/dist/base/data-types/field-types.js.map +1 -1
  68. package/dist/base/data-types/internal/relation-endpoints.js +1 -0
  69. package/dist/base/data-types/internal/relation-endpoints.js.map +1 -1
  70. package/dist/base/data-types/status-type.d.ts +5 -0
  71. package/dist/base/data-types/status-type.js +5 -0
  72. package/dist/base/data-types/status-type.js.map +1 -1
  73. package/dist/base/display.d.ts +6 -6
  74. package/dist/base/empty-state.d.ts +15 -13
  75. package/dist/base/empty-state.js +2 -0
  76. package/dist/base/empty-state.js.map +1 -1
  77. package/dist/base/form-block-schema.d.ts +50 -50
  78. package/dist/base/form-schema.d.ts +2 -0
  79. package/dist/base/form-schema.js +2 -0
  80. package/dist/base/form-schema.js.map +1 -1
  81. package/dist/base/grid.d.ts +4 -0
  82. package/dist/base/grid.js +4 -0
  83. package/dist/base/grid.js.map +1 -1
  84. package/dist/base/guard.d.ts +30 -28
  85. package/dist/base/guard.js +2 -0
  86. package/dist/base/guard.js.map +1 -1
  87. package/dist/base/helpers/archive-mode-helpers.d.ts +1 -1
  88. package/dist/base/helpers/file-refs.js +15 -15
  89. package/dist/base/helpers/file-refs.js.map +1 -1
  90. package/dist/base/internal/form-schema-validation.js +7 -1
  91. package/dist/base/internal/form-schema-validation.js.map +1 -1
  92. package/dist/base/internal/realtime-topics.js.map +1 -1
  93. package/dist/base/internal/searchable.d.ts +2 -1
  94. package/dist/base/internal/searchable.js +1 -3
  95. package/dist/base/internal/searchable.js.map +1 -1
  96. package/dist/base/key-value-list.d.ts +21 -19
  97. package/dist/base/key-value-list.js +2 -0
  98. package/dist/base/key-value-list.js.map +1 -1
  99. package/dist/base/kpi-card.d.ts +4 -2
  100. package/dist/base/kpi-card.js +2 -0
  101. package/dist/base/kpi-card.js.map +1 -1
  102. package/dist/base/live-toggle.d.ts +2 -0
  103. package/dist/base/live-toggle.js +2 -0
  104. package/dist/base/live-toggle.js.map +1 -1
  105. package/dist/base/meter.d.ts +23 -21
  106. package/dist/base/meter.js +2 -0
  107. package/dist/base/meter.js.map +1 -1
  108. package/dist/base/nav-card-grid.d.ts +24 -19
  109. package/dist/base/nav-card-grid.js +5 -0
  110. package/dist/base/nav-card-grid.js.map +1 -1
  111. package/dist/base/page-toolbar.d.ts +2 -0
  112. package/dist/base/page-toolbar.js +2 -0
  113. package/dist/base/page-toolbar.js.map +1 -1
  114. package/dist/base/period-selector.d.ts +3 -1
  115. package/dist/base/period-selector.js +2 -0
  116. package/dist/base/period-selector.js.map +1 -1
  117. package/dist/base/placeholder.d.ts +2 -0
  118. package/dist/base/placeholder.js +2 -0
  119. package/dist/base/placeholder.js.map +1 -1
  120. package/dist/base/resource-form-schema.d.ts +2 -2
  121. package/dist/base/searchable.d.ts +3 -2
  122. package/dist/base/searchable.js +2 -2
  123. package/dist/base/searchable.js.map +1 -1
  124. package/dist/base/section.d.ts +6 -2
  125. package/dist/base/section.js +4 -0
  126. package/dist/base/section.js.map +1 -1
  127. package/dist/base/stack.d.ts +6 -0
  128. package/dist/base/stack.js +6 -0
  129. package/dist/base/stack.js.map +1 -1
  130. package/dist/base/stat-group.d.ts +16 -14
  131. package/dist/base/stat-group.js +2 -0
  132. package/dist/base/stat-group.js.map +1 -1
  133. package/dist/base/tab.d.ts +10 -8
  134. package/dist/base/tab.js +2 -0
  135. package/dist/base/tab.js.map +1 -1
  136. package/dist/base/table-view/column-display.d.ts +1 -1
  137. package/dist/base/table-view/column-display.js.map +1 -1
  138. package/dist/base/table-view/data-functions.d.ts +10 -4
  139. package/dist/base/table-view/data-functions.js.map +1 -1
  140. package/dist/base/table-view/factory.d.ts +5 -0
  141. package/dist/base/table-view/factory.js +8 -4
  142. package/dist/base/table-view/factory.js.map +1 -1
  143. package/dist/base/table-view/internal/auth.d.ts +17 -3
  144. package/dist/base/table-view/internal/auth.js +26 -5
  145. package/dist/base/table-view/internal/auth.js.map +1 -1
  146. package/dist/base/table-view/internal/column-conditions.d.ts +8 -1
  147. package/dist/base/table-view/internal/column-conditions.js +25 -13
  148. package/dist/base/table-view/internal/column-conditions.js.map +1 -1
  149. package/dist/base/table-view/internal/column-display.d.ts +1 -2
  150. package/dist/base/table-view/internal/column-display.js +3 -9
  151. package/dist/base/table-view/internal/column-display.js.map +1 -1
  152. package/dist/base/table-view/internal/factory-helpers.d.ts +1 -1
  153. package/dist/base/table-view/internal/factory-helpers.js +17 -14
  154. package/dist/base/table-view/internal/factory-helpers.js.map +1 -1
  155. package/dist/base/table-view/internal/factory-lifecycle-created.d.ts +2 -3
  156. package/dist/base/table-view/internal/factory-lifecycle-created.js +5 -5
  157. package/dist/base/table-view/internal/factory-lifecycle-created.js.map +1 -1
  158. package/dist/base/table-view/internal/factory-lifecycle-filter.d.ts +2 -2
  159. package/dist/base/table-view/internal/factory-private-actions.d.ts +2 -2
  160. package/dist/base/table-view/internal/files.d.ts +2 -1
  161. package/dist/base/table-view/internal/files.js +14 -5
  162. package/dist/base/table-view/internal/files.js.map +1 -1
  163. package/dist/base/table-view/internal/footer.d.ts +3 -3
  164. package/dist/base/table-view/internal/footer.js +16 -15
  165. package/dist/base/table-view/internal/footer.js.map +1 -1
  166. package/dist/base/table-view/internal/guards.d.ts +27 -5
  167. package/dist/base/table-view/internal/guards.js +127 -49
  168. package/dist/base/table-view/internal/guards.js.map +1 -1
  169. package/dist/base/table-view/internal/meta-helpers.d.ts +1 -1
  170. package/dist/base/table-view/internal/meta-helpers.js +3 -4
  171. package/dist/base/table-view/internal/meta-helpers.js.map +1 -1
  172. package/dist/base/table-view/internal/meta.js +2 -3
  173. package/dist/base/table-view/internal/meta.js.map +1 -1
  174. package/dist/base/table-view/internal/option-contract.d.ts +1 -1
  175. package/dist/base/table-view/internal/option-footer.d.ts +2 -2
  176. package/dist/base/table-view/internal/option-row-actions.d.ts +1 -1
  177. package/dist/base/table-view/internal/realtime.d.ts +52 -11
  178. package/dist/base/table-view/internal/realtime.js +23 -16
  179. package/dist/base/table-view/internal/realtime.js.map +1 -1
  180. package/dist/base/table-view/internal/resource-form.d.ts +1 -1
  181. package/dist/base/table-view/internal/resource-form.js +1 -1
  182. package/dist/base/table-view/internal/resource-form.js.map +1 -1
  183. package/dist/base/table-view/internal/row-actions-schema.d.ts +12 -12
  184. package/dist/base/table-view/internal/row-rule-shape.d.ts +11 -5
  185. package/dist/base/table-view/internal/row-rule-shape.js +60 -30
  186. package/dist/base/table-view/internal/row-rule-shape.js.map +1 -1
  187. package/dist/base/table-view/internal/row-rules.d.ts +28 -1
  188. package/dist/base/table-view/internal/row-rules.js +8 -19
  189. package/dist/base/table-view/internal/row-rules.js.map +1 -1
  190. package/dist/base/table-view/internal/tabs.d.ts +1 -1
  191. package/dist/base/table-view/internal/tabs.js +1 -4
  192. package/dist/base/table-view/internal/tabs.js.map +1 -1
  193. package/dist/base/table-view/internal/validation.d.ts +2 -2
  194. package/dist/base/table-view/internal/validation.js +7 -7
  195. package/dist/base/table-view/internal/validation.js.map +1 -1
  196. package/dist/base/table-view/meta.d.ts +7 -6
  197. package/dist/base/table-view/meta.js +12 -7
  198. package/dist/base/table-view/meta.js.map +1 -1
  199. package/dist/base/table-view/resource-form.d.ts +3 -1
  200. package/dist/base/table-view/resource-form.js +2 -0
  201. package/dist/base/table-view/resource-form.js.map +1 -1
  202. package/dist/base/table-view/routes.d.ts +73 -21
  203. package/dist/base/table-view/routes.js +13 -10
  204. package/dist/base/table-view/routes.js.map +1 -1
  205. package/dist/base/table-view/schema.d.ts +152 -152
  206. package/dist/base/table-view/source.js +4 -1
  207. package/dist/base/table-view/source.js.map +1 -1
  208. package/dist/base/table-view.d.ts +1 -1
  209. package/dist/base/table-view.js.map +1 -1
  210. package/dist/base/top-list-card.d.ts +35 -33
  211. package/dist/base/top-list-card.js +2 -0
  212. package/dist/base/top-list-card.js.map +1 -1
  213. package/dist/base/types/error.js +5 -2
  214. package/dist/base/types/error.js.map +1 -1
  215. package/dist/base/types/guards.d.ts +4 -4
  216. package/dist/base/types/json.d.ts +1 -10
  217. package/dist/base/types/row-action.d.ts +7 -7
  218. package/dist/component-options/confirm-dialog.d.ts +2 -0
  219. package/dist/component-options/confirm-dialog.js.map +1 -1
  220. package/dist/component-options/data-type.d.ts +2 -2
  221. package/dist/component-options/data-type.js +7 -7
  222. package/dist/component-options/data-type.js.map +1 -1
  223. package/dist/component-options/row-action.d.ts +14 -9
  224. package/dist/component-slots.d.ts +11 -2
  225. package/dist/component-slots.js +1 -0
  226. package/dist/component-slots.js.map +1 -1
  227. package/dist/component.d.ts +7 -8
  228. package/dist/component.js +17 -25
  229. package/dist/component.js.map +1 -1
  230. package/dist/data-controllers/members.d.ts +11 -7
  231. package/dist/data-controllers/members.js +1 -1
  232. package/dist/data-controllers/members.js.map +1 -1
  233. package/dist/data-controllers/roles.d.ts +32 -10
  234. package/dist/db/internal/exportJobs.model.d.ts +7 -5
  235. package/dist/db/internal/inviteResolutions.model.d.ts +7 -5
  236. package/dist/db/internal/tenantLifecycle.model.d.ts +7 -5
  237. package/dist/db/models/roles.model.d.ts +7 -5
  238. package/dist/db/models/tenantMembers.model.d.ts +7 -5
  239. package/dist/db/models/tenants.model.d.ts +7 -5
  240. package/dist/db/models/user_invites.model.d.ts +7 -5
  241. package/dist/db/tables/tenants.table.d.ts +9 -0
  242. package/dist/db/tables/tenants.table.js +4 -0
  243. package/dist/db/tables/tenants.table.js.map +1 -1
  244. package/dist/guards.d.ts +1 -1
  245. package/dist/hooks.d.ts +10 -9
  246. package/dist/hooks.js +42 -33
  247. package/dist/hooks.js.map +1 -1
  248. package/dist/internal/attachments.d.ts +2 -2
  249. package/dist/internal/component-builder.js +2 -1
  250. package/dist/internal/component-builder.js.map +1 -1
  251. package/dist/internal/component-serialization.d.ts +2 -4
  252. package/dist/internal/component-serialization.js +4 -3
  253. package/dist/internal/component-serialization.js.map +1 -1
  254. package/dist/internal/component-slots.d.ts +19 -2
  255. package/dist/internal/component-slots.js +35 -12
  256. package/dist/internal/component-slots.js.map +1 -1
  257. package/dist/internal/component-watch-filter.d.ts +11 -0
  258. package/dist/internal/component-watch-filter.js +22 -0
  259. package/dist/internal/component-watch-filter.js.map +1 -0
  260. package/dist/internal/component-watches.js +0 -1
  261. package/dist/internal/component-watches.js.map +1 -1
  262. package/dist/internal/own-entry.d.ts +7 -0
  263. package/dist/internal/own-entry.js +13 -0
  264. package/dist/internal/own-entry.js.map +1 -0
  265. package/dist/invite-extension-payloads.d.ts +14 -2
  266. package/dist/invite-extensions/internal/delivery.js +1 -2
  267. package/dist/invite-extensions/internal/delivery.js.map +1 -1
  268. package/dist/invite-extensions/internal/edit.d.ts +2 -1
  269. package/dist/invite-extensions/internal/edit.js +8 -9
  270. package/dist/invite-extensions/internal/edit.js.map +1 -1
  271. package/dist/invite-extensions/internal/form-slot.js +8 -15
  272. package/dist/invite-extensions/internal/form-slot.js.map +1 -1
  273. package/dist/invite-extensions/internal/payload-slices.d.ts +33 -3
  274. package/dist/invite-extensions/internal/payload-slices.js +84 -19
  275. package/dist/invite-extensions/internal/payload-slices.js.map +1 -1
  276. package/dist/invite-extensions/internal/types.d.ts +10 -1
  277. package/dist/invite-resolution.js +6 -4
  278. package/dist/invite-resolution.js.map +1 -1
  279. package/dist/json.d.ts +10 -0
  280. package/dist/json.js +3 -0
  281. package/dist/json.js.map +1 -0
  282. package/dist/layout-banners.d.ts +5 -2
  283. package/dist/layout-banners.js.map +1 -1
  284. package/dist/page/controllers.d.ts +2 -2
  285. package/dist/page/internal/extension-validation.js +3 -2
  286. package/dist/page/internal/extension-validation.js.map +1 -1
  287. package/dist/page/internal/metadata-layout.js +1 -1
  288. package/dist/page/internal/metadata-layout.js.map +1 -1
  289. package/dist/page/internal/metadata-publish.js +3 -2
  290. package/dist/page/internal/metadata-publish.js.map +1 -1
  291. package/dist/page/internal/metadata-registration.js +5 -4
  292. package/dist/page/internal/metadata-registration.js.map +1 -1
  293. package/dist/page/internal/metadata-upload-registration.d.ts +3 -1
  294. package/dist/page/internal/metadata-upload-registration.js +9 -0
  295. package/dist/page/internal/metadata-upload-registration.js.map +1 -1
  296. package/dist/page/internal/metadata-uploads.d.ts +18 -5
  297. package/dist/page/internal/metadata-uploads.js +7 -8
  298. package/dist/page/internal/metadata-uploads.js.map +1 -1
  299. package/dist/page/internal/page-record.d.ts +2 -3
  300. package/dist/page/internal/page-record.js +19 -14
  301. package/dist/page/internal/page-record.js.map +1 -1
  302. package/dist/page/layout-filter.js +17 -6
  303. package/dist/page/layout-filter.js.map +1 -1
  304. package/dist/page/metadata.js +5 -7
  305. package/dist/page/metadata.js.map +1 -1
  306. package/dist/request-tenant.d.ts +10 -4
  307. package/dist/request-tenant.js +16 -34
  308. package/dist/request-tenant.js.map +1 -1
  309. package/dist/tenant-export.d.ts +2 -1
  310. package/dist/tenant-lifecycle.d.ts +13 -4
  311. package/dist/tenant-lifecycle.js +51 -18
  312. package/dist/tenant-lifecycle.js.map +1 -1
  313. package/dist/types.d.ts +20 -0
  314. package/dist/uploads.js +42 -59
  315. package/dist/uploads.js.map +1 -1
  316. package/dist/utils/fire-and-forget.js.map +1 -1
  317. package/dist/utils/internal/decorated-class.d.ts +14 -0
  318. package/dist/utils/internal/decorated-class.js +37 -0
  319. package/dist/utils/internal/decorated-class.js.map +1 -0
  320. package/dist/utils/internal/type-check.d.ts +9 -0
  321. package/dist/utils/internal/type-check.js +13 -5
  322. package/dist/utils/internal/type-check.js.map +1 -1
  323. package/dist/utils/internal/value-parser.d.ts +1 -1
  324. package/dist/utils/internal/value-parser.js +22 -9
  325. package/dist/utils/internal/value-parser.js.map +1 -1
  326. package/dist/watch-actions.d.ts +1 -1
  327. package/docs/1.core/1.introduction.md +82 -77
  328. package/docs/1.core/10.layout-banners.md +57 -45
  329. package/docs/1.core/11.modules-and-dynamic-menus.md +178 -0
  330. package/docs/1.core/12.frontend-modules-and-request-helpers.md +155 -0
  331. package/docs/1.core/13.tenant-data-export.md +120 -0
  332. package/docs/1.core/2.pages-and-modules.md +140 -367
  333. package/docs/1.core/3.components.md +21 -22
  334. package/docs/1.core/4.permissions.md +92 -71
  335. package/docs/1.core/5.tenancy.md +112 -86
  336. package/docs/1.core/6.invites.md +90 -65
  337. package/docs/1.core/7.hooks.md +96 -166
  338. package/docs/1.core/8.realtime.md +44 -45
  339. package/docs/1.core/9.quick-actions.md +37 -30
  340. package/docs/2.auth/1.introduction.md +33 -35
  341. package/docs/2.auth/2.authentication.md +60 -38
  342. package/docs/2.auth/3.tokens-and-sessions.md +56 -55
  343. package/docs/2.auth/4.users.md +82 -52
  344. package/docs/2.auth/5.external-identities.md +25 -26
  345. package/docs/3.base/1.introduction.md +64 -60
  346. package/docs/3.base/10.display-blocks.md +15 -16
  347. package/docs/3.base/11.page-layouts.md +273 -0
  348. package/docs/3.base/12.table-row-actions.md +122 -0
  349. package/docs/3.base/13.table-filters-and-forms.md +104 -0
  350. package/docs/3.base/14.table-events-and-helpers.md +114 -0
  351. package/docs/3.base/15.table-sources-and-gates.md +24 -0
  352. package/docs/3.base/2.types.md +44 -41
  353. package/docs/3.base/3.forms.md +27 -28
  354. package/docs/3.base/4.tables.md +34 -367
  355. package/docs/3.base/5.charts-and-widgets.md +28 -29
  356. package/docs/3.base/6.layout.md +21 -270
  357. package/docs/3.base/8.data-types.md +39 -40
  358. package/docs/3.base/9.helpers.md +36 -38
  359. package/docs/4.notifications/1.introduction.md +22 -26
  360. package/docs/4.notifications/2.categories-and-subjects.md +58 -55
  361. package/docs/4.notifications/3.sending.md +72 -68
  362. package/docs/5.html-render/1.introduction.md +119 -81
  363. package/docs/5.html-render/2.built-in-emails.md +71 -51
  364. package/docs/index.md +18 -29
  365. package/docs/tsconfig.json +4 -0
  366. package/package.json +12 -12
  367. package/docs/images/emails-admin-invite.png +0 -0
  368. package/docs/images/emails-change-notice.png +0 -0
  369. package/docs/images/emails-change-verification.png +0 -0
  370. package/docs/images/emails-digest-logo-dark.png +0 -0
  371. package/docs/images/emails-digest-logo-light.png +0 -0
  372. package/docs/images/emails-digest.png +0 -0
  373. package/docs/images/emails-export-partial.png +0 -0
  374. package/docs/images/emails-export-ready.png +0 -0
  375. package/docs/images/emails-reset-password.png +0 -0
  376. package/docs/images/emails-two-factor.png +0 -0
  377. package/docs/images/emails-user-validation.png +0 -0
@@ -1,25 +1,20 @@
1
1
  ---
2
- title: "Pages and Modules"
3
- description: "Pages are controller classes registered into a navigation tree of modules and categories, with their own routes, layouts and extensions."
2
+ title: "Pages and modules"
3
+ description: "Pages are controller classes registered into a navigation tree of categories, with their own routes, layouts, access rules and extensions."
4
+ mode: reference
4
5
  ---
5
6
 
6
- # Pages and Modules
7
+ A page is a controller class. `PageController(id, options, layout?)` builds a base class bound to a URL, `@RegisterPage()` records it, and the components declared as static fields on the class become the page's serialized layout. Categories group pages into the sidebar tree. Modules, which own a branch of that tree, and dynamic menu entries are described in [Modules and dynamic menus](/docs/interfaces/core/modules-and-dynamic-menus).
7
8
 
8
- ## Overview
9
+ To build a first screen, follow [Pages & components](/docs/building/pages-and-components). That guide also shows [page extension placement](/docs/building/pages-and-components#extending-another-modules-page) in the running playground. Everything on this page lives in `@antelopejs/interface-dms/page`.
9
10
 
10
- A page is a controller class. `PageController(id, options, layout?)` builds a base class bound to a URL, `@RegisterPage()` records it, and the components declared as static fields on the class become the page's serialized layout. Categories group pages into the sidebar tree, and modules group a whole set of pages and categories under their own root.
11
+ Registration happens at import time. The `@RegisterPage()` decorator runs when the file loads, so the order in which you import page files sets the order in which pages, categories and their permissions register.
11
12
 
12
- Building your first screen? Follow [Pages & components](/docs/building/pages-and-components) first. This reference covers registration, identifiers, and lifecycle rules. The guide also shows [page-extension placement](/docs/building/pages-and-components#extending-another-modules-page) in the running playground.
13
+ ## Declare a page
13
14
 
14
- Everything on this page lives in `@antelopejs/interface-dms/page`.
15
+ `PageController(id, options, layout?)` returns a class that extends `Controller(pageRoutePath(fullSlug))`, so its routes live under `PAGE_ROUTE_PREFIX` (`/dms/pages`). Extend it, decorate it with `@RegisterPage()`, and declare components as static fields.
15
16
 
16
- Registration happens at import time: the `@RegisterPage()` decorator runs when the file is loaded, so the order in which you import page files determines the order in which pages, categories and their permissions are registered.
17
-
18
- ## Declare a Page
19
-
20
- `PageController(id, options, layout?)` returns a class extending `Controller(fullSlug)`. Extend it, decorate with `@RegisterPage()`, and declare components as static fields.
21
-
22
- ```typescript
17
+ ```typescript fragment
23
18
  import {
24
19
  Category,
25
20
  PageController,
@@ -46,44 +41,58 @@ export class InvoicesPage extends PageController("invoices", {
46
41
  }
47
42
  ```
48
43
 
49
- `PageController` requires either `category` or `module` in its options and throws otherwise. Since the returned class extends `Controller`, route decorators from `@antelopejs/interface-api` work on it directly — a route declared on the page class is served under the page's slug.
44
+ `PageController` throws when its options declare neither `category` nor `module`. The returned class extends `Controller`, so route decorators from `@antelopejs/interface-api` work on it directly, and a route declared on the page class is served under the page's slug. [Identifiers a page carries](#identifiers-a-page-carries) gives the exact path.
50
45
 
51
- The third argument is the page layout, a `ComponentInfo` produced by a layout builder such as `DefaultLayout()` from `@antelopejs/interface-dms/base`. When you omit it, the page uses its own `layout` option, then the `layout` option of the nearest category or parent page declaring one, then `DefaultLayout()`.
46
+ The third argument is the page layout, a `ComponentInfo` from a layout builder such as `DefaultLayout()` from `@antelopejs/interface-dms/base`. Without it, the page uses its own `layout` option, then the `layout` of the nearest category or parent page that declares one, then `DefaultLayout()`.
47
+
48
+ `@RegisterPage()` reads the class's own static properties and records every `Component` under its property name. It skips a property whose name starts with `_`, so a class can keep a helper value without turning it into a layout entry.
52
49
 
53
50
  ### `MenuOptions`
54
51
 
55
52
  | Option | Type | Description |
56
53
  | ------ | ---- | ----------- |
57
54
  | `displayName` | `string` | Label shown in the sidebar. Required. |
58
- | `category` | `CategoryInfo \| ControllerClass` | Parent category. A `CategoryInfo` from `Category`/`RootCategory`/`RegisterModule`, or a controller class produced by `PageController`/`RootPageController`. |
59
- | `module` | `string` | Id of a module registered with `RegisterModule`. Attaches the page to that module's tree. |
55
+ | `category` | `CategoryInfo \| ControllerClass` | Parent category: a `CategoryInfo` from `Category`, `RootCategory` or `RegisterModule`, or a controller class from `PageController` or `RootPageController`. |
56
+ | `module` | `string` | Id of a module registered with `RegisterModule`. Attaches the entry to that module's tree. |
60
57
  | `urlSlug` | `string` | URL segment for this entry. Defaults to `id`. |
61
58
  | `description` | `string` | Longer description carried in the page metadata. |
62
59
  | `order` | `number` | Sort order among siblings. |
63
60
  | `icon` | `string` | Icon name. |
64
- | `hidden` | `boolean` | Keep the entry out of the menu. Inherited from the resolved category when that category is hidden. |
65
- | `parent` | `ControllerClass` | The page this one is a detail of: the breadcrumb shows it before this page ("Operations / Projects / <record>"), its route parameters filled from this page's own. Resolved at registration, served as `parentSlug`. Pages only. |
66
- | `permission` | `Partial<Permission> \| Action` | Override the auto-derived page permission, or bind the page to an existing component `Action`. See [Permissions](/docs/interfaces/core/permissions). |
67
- | `publicAccess` | `boolean` | Serve the page without authentication. Inherited from the resolved category. |
68
- | `authOnly` | `boolean` | Require a logged-in user but no permission check. Inherited from the resolved category. |
69
- | `memberAccess` | `boolean \| "self"` | Every signed-in member holds the page, its components and their actions without a role grant: their permissions register as `defaultGranted`, so guards still run and the Roles editor leaves them out. `true` is inherited by what is declared under the entry, page or category. `"self"` opens the entry alone: the settings root declares it, so it opens itself, not the pages declared under it. |
61
+ | `hidden` | `boolean` | Keeps the entry out of the menu. A page or category under a hidden category is hidden too. |
62
+ | `parent` | `ControllerClass` | The page this one is a detail of. The breadcrumb shows it before this page ("Operations / Projects / <record>"), with its route parameters filled from this page's parameters of the same name. Resolved at registration and served as `parentSlug`. Read on pages only. |
63
+ | `permission` | `Partial<Permission> \| Action` | Overrides the derived page permission, or binds the page to an existing component `Action`. See [Permissions](/docs/interfaces/core/permissions). |
64
+ | `publicAccess` | `boolean` | Serves the page without authentication. A page inherits it from its category. |
65
+ | `authOnly` | `boolean` | Requires a signed-in user and no permission. A page inherits it from its category. |
66
+ | `memberAccess` | `boolean \| "self"` | Opens the entry, its components and their actions to every signed-in member without a role grant. Their permissions register as `defaultGranted`, so guards still check them, the tenant access gate still applies, and the roles editor does not list them. `true` passes to what is declared under the entry. `"self"` opens the entry alone. `settingsCategory` declares `"self"`, so every member opens the settings overview while the workspace pages under it stay gated. `MemberAccess` names this type. |
70
67
  | `type` | `MenuItemType` | `"label"` or `"link"`. |
71
68
  | `validation` | `PageValidation` | Preconditions for opening the page. |
72
69
  | `setupId` | `string` | Id of a page setup function to run while the page is mounted. |
73
- | `noComponentPermissions` | `boolean` | Do not register per-component permissions for this page. |
70
+ | `noComponentPermissions` | `boolean` | Registers no permission for the page's components. |
74
71
  | `query` | `MenuItemQuery` | Query parameters carried by the entry's menu link. |
75
72
  | `variant` | `MenuItemVariant` | Semantic look of the entry. |
76
- | `status` | `MenuItemStatus` | Semantic state dot rendered at the end of the entry. |
77
- | `badge` | `string` | Short count or label at the end of the entry (`3`, `new`); wins over `status` and over a counted navigation badge. |
78
- | `layout` | `ComponentInfo` | Default layout of the entry and of every page and sub-category declared under it that sets none of its own. The nearest declaring ancestor wins; a page's layout argument always overrides it. |
79
- | `bypassTenantAccessGate` | `boolean` | Keep the page reachable while a tenant access gate denies the tenant. Inherited by the pages of a flagged category. |
80
- | `live` | `PageLiveOptions` | Gives the page a Live / Pause state over the realtime of its blocks: `{ maxRate?, slowRateAfter?, rememberKey? }`. `maxRate` is the shortest interval in seconds between two refetches of one block, and the polling interval while the realtime stream is down; `slowRateAfter` (`PageLiveSlowRate`: `{ period: PageLivePeriod, maxRate }`, `period` like `"7d"`) a slower one once the period shown spans `period`; `rememberKey` a key sharing the remembered choice between pages. Read on pages only. A rate that is not a positive number, a period not reading like `30m` / `24h` / `7d`, or an empty `rememberKey` throws at declaration. |
73
+ | `status` | `MenuItemStatus` | Semantic state dot at the end of the entry. |
74
+ | `badge` | `string` | Short count or label at the end of the entry (`3`, `new`). It takes the place of `status` and of a counted navigation badge. |
75
+ | `layout` | `ComponentInfo` | Default layout of the entry and of every page and category under it that sets none. The nearest declaring ancestor wins, and a page's layout argument always overrides it. |
76
+ | `bypassTenantAccessGate` | `boolean` | Keeps the page reachable while a tenant access gate denies the tenant. The pages of a flagged category inherit it. |
77
+ | `live` | `PageLiveOptions` | Gives the page a Live and Pause state over the realtime of its blocks. Read on pages only. |
81
78
 
82
79
  `MenuItemType` is `"label" | "link"`.
83
80
 
84
- `PageValidation` has three optional fields: `requiredQueryParams` (a `string[]` of query parameters the page needs), `customFunctionId` (the id of a registered validation function) and `redirectTo` (where a page failing its validation sends the user instead of answering "Page not found": a dashboard path, with the page URL's `{{params.X}}` / `{{query.X}}` tokens; see [Page setup and validation](/docs/building/pages-and-components#page-setup-and-validation)).
81
+ `PageValidation` has three optional fields:
82
+
83
+ - `requiredQueryParams`, a `string[]` of query parameters the page needs, each with a non-empty value.
84
+ - `customFunctionId`, the id of a validation function the frontend registers.
85
+ - `redirectTo`, the dashboard path a page that fails its validation sends the user to instead of answering "Page not found". The path may use the `{{params.X}}` and `{{query.X}}` tokens of the page URL. [Page setup and validation](/docs/building/pages-and-components#page-setup-and-validation) shows it in use.
86
+
87
+ `PageLiveOptions` has three optional fields. [Live pages](/docs/building/live-pages) explains the behaviour they control.
88
+
89
+ - `maxRate`, the shortest interval in seconds between two refetches of one block, and the polling interval while the realtime stream is down.
90
+ - `slowRateAfter`, a `PageLiveSlowRate` of `{ period: PageLivePeriod; maxRate: number }`. Once the period the page shows spans `period`, its `maxRate` replaces the page's. `PageLivePeriod` is a whole number followed by `m`, `h` or `d`, such as `"7d"`.
91
+ - `rememberKey`, a key that shares the remembered Live or Pause choice between the pages that declare it.
85
92
 
86
- ### Menu Entry Appearance
93
+ A rate that is not a positive number, a period that does not read like `30m`, `24h` or `7d`, or an empty `rememberKey` throws at declaration.
94
+
95
+ ### Menu entry appearance
87
96
 
88
97
  Three types describe how an entry looks, so a consumer never ships raw classes:
89
98
 
@@ -93,11 +102,13 @@ type MenuItemStatus = "success" | "warning" | "error" | "neutral";
93
102
  type MenuItemQuery = Record<string, string>;
94
103
  ```
95
104
 
96
- `accent` renders the entry in the theme's primary color, for a call-to-action entry such as "New project". `status` maps to the theme's semantic colors.
105
+ `accent` renders the entry in the theme's primary color, for a call to action such as "New project". `status` maps to the theme's semantic colors.
97
106
 
98
- `query` appends parameters to the entry's link, which is what lets a single page serve many entries — declare it with `validation.requiredQueryParams`, then point several entries at it:
107
+ `query` appends parameters to the entry's link, so one page can serve many entries. Declare the parameter in `validation.requiredQueryParams`, then point several entries at the page:
99
108
 
100
109
  ```typescript
110
+ import { PageController, RegisterPage, pagesCategory } from "@antelopejs/interface-dms/page";
111
+
101
112
  @RegisterPage()
102
113
  export class ProjectPage extends PageController("project", {
103
114
  displayName: "Project",
@@ -107,15 +118,15 @@ export class ProjectPage extends PageController("project", {
107
118
  }) {}
108
119
  ```
109
120
 
110
- ### Reaching a Page Behind a Tenant Gate
121
+ ### Reach a page behind a tenant gate
111
122
 
112
- `bypassTenantAccessGate` is the page-level mirror of the guard option of the same name ([Permissions](/docs/interfaces/core/permissions)). It covers page visibility, menu visibility and the page's own layout route, and a flagged category passes it to its pages. Permission checks still apply — unlike `authOnly`, which drops them entirely.
123
+ `bypassTenantAccessGate` is the page-level mirror of the guard option of the same name ([Permissions](/docs/interfaces/core/permissions#authtenantowner-and-authtenantmember)). It covers the page's visibility, its menu entry and its own layout route, and a flagged category passes it to its pages. Permission checks still apply. `authOnly`, by contrast, drops them.
113
124
 
114
- It does not cover the data routes feeding the page: a data API is not owned by a single page, so it opts out through its own guard, or through `TableViewOptions.bypassTenantAccessGate` for a table view. Reserve the flag for recovery surfaces — billing, a payment portal — that a blocked tenant must still reach in order to unblock itself.
125
+ The flag does not cover the data routes that feed the page. A data API belongs to no single page, so it opts out through its own guard, or through `TableViewOptions.bypassTenantAccessGate` for a table view. Reserve the flag for recovery surfaces, such as billing or a payment portal, that a blocked tenant must reach to unblock itself.
115
126
 
116
- ## Group Pages with Categories
127
+ ## Group pages with categories
117
128
 
118
- `Category(id, options)` returns a `CategoryInfo` you pass as another entry's `category`. Categories nest, and they require a `category` or a `module` of their own.
129
+ `Category(id, options)` returns a `CategoryInfo` that you pass as another entry's `category`. Categories nest, and each one throws unless it declares a `category` or a `module`.
119
130
 
120
131
  ```typescript
121
132
  import {
@@ -135,11 +146,11 @@ export const contacts = Category("contacts", {
135
146
  });
136
147
  ```
137
148
 
138
- Creating a category registers a permission keyed to its `fullId`, unless the category sits inside a module — see [Permissions](/docs/interfaces/core/permissions).
149
+ Creating a category registers a permission keyed to its `fullId`. The permission is `defaultGranted` when the category declares `publicAccess` or `memberAccess`, and its `permission` option overrides the derived fields. A category inside a module marks its id module-scoped instead, and a category whose `permission` is an `Action` registers none. [Permissions](/docs/interfaces/core/permissions) covers both.
139
150
 
140
- ### Root Categories
151
+ ### Root categories
141
152
 
142
- `RootCategory(id, options)` creates a top-level group next to the built-in ones. Its options are `MenuOptions` without `category` and `module`. `type` defaults to `"label"`. The ids `pages`, `modules` and `settings` are reserved and throw.
153
+ `RootCategory(id, options)` creates a top-level group next to the built-in ones. Its options are `MenuOptions` without `category` and `module`, and `type` defaults to `"label"`. The ids `pages`, `modules` and `settings` are reserved and throw.
143
154
 
144
155
  ```typescript
145
156
  import { RootCategory } from "@antelopejs/interface-dms/page";
@@ -151,187 +162,48 @@ export const catalogCategory = RootCategory("catalog", {
151
162
  });
152
163
  ```
153
164
 
154
- The interface exports three built-in roots:
165
+ The interface exports these built-in roots:
155
166
 
156
- | Export | Kind | URL slug |
157
- | ------ | ---- | -------- |
158
- | `pagesCategory` | `CategoryInfo` | `/` |
159
- | `modulesCategory` | controller class (`RootPageController`) | `/modules` |
160
- | `settingsCategory` | controller class (`RootPageController`) | `/settings` |
167
+ | Export | Kind | URL slug | `order` |
168
+ | ------ | ---- | -------- | ------- |
169
+ | `pagesCategory` | `CategoryInfo` | `/` | `1` |
170
+ | `modulesCategory` | controller class (`RootPageController`) | `/modules` | `2` |
171
+ | `settingsCategory` | controller class (`RootPageController`) | `/settings` | `3` |
161
172
 
162
- `settingsCategory` declares `layout: SettingsLayout()`, so every page under it renders in the settings frame unless it passes a layout of its own. `workspaceSettingsCategory` (`Category("workspace", …)` under `settingsCategory`, full id `settings.workspace`, URL `/settings/workspace`) holds the workspace settings — members, invitations, roles — and is where a project adds its own ([Settings pages](/docs/building/settings-pages)).
173
+ `settingsCategory` declares `layout: SettingsLayout()`, so every page under it renders in the settings frame unless it passes its own layout. `workspaceSettingsCategory` is `Category("workspace", …)` under `settingsCategory`, with the full id `settings.workspace` and the URL `/settings/workspace`. It holds the workspace settings, such as members, invitations and roles, and a project adds its own there ([Settings pages](/docs/building/settings-pages)).
163
174
 
164
- `modulesCategory` is the module root: every module registered with `RegisterModule` attaches under it. A page or category that declares a `module` cannot resolve to a category under `settingsCategory`; that combination throws.
175
+ `modulesCategory` is the module root that every module registered with `RegisterModule` attaches under. A page or category that declares a `module` cannot resolve to a category under `settingsCategory`. That combination throws.
165
176
 
166
177
  ### `RootPageController`
167
178
 
168
- `RootPageController(id, options, layout?)` builds a page controller at the root of the tree. Its options are `MenuOptions` without `category` and `module`, plus an optional `isModuleRoot` boolean. Unlike `PageController` it does not require a parent, which is what makes it usable for a top-level landing screen.
169
-
170
- ## Identifiers a Page Carries
171
-
172
- `Category`, `PageController` and `RootPageController` compute two identifiers from the entry's own id and its parent chain:
173
-
174
- - `fullId` — the dot-joined chain of ids, starting at the root category (`pages.crm.contacts`). This is the page's permission id and the key realtime topics are registered under.
175
- - `fullSlug` — the joined URL slugs (`/crm/contacts`).
176
-
177
- `CategoryInfo` extends `MenuOptions` with `category` redeclared as a required `CategoryInfo | ControllerClass | undefined` holding the resolved parent, and adds `id`, `fullId`, `fullSlug` and an optional `isModuleRoot`. `PageInfo` extends `CategoryInfo` and adds `layoutUrl`, the URL the page's component tree is served from — `/dms/pages`, the page's `fullSlug`, then `/pagelayout`.
178
-
179
- A page controller (`PageController`, `RootPageController`) serves its routes under `PAGE_ROUTE_PREFIX` (`/dms/pages`), not at the page's own path: the frontend server answers that path with the page and proxies only the backend prefixes declared to it, `/api/**` and `/dms/**` for the DMS. `@Get("/sessions")` on the page `/settings/user/security` is `GET /dms/pages/settings/user/security/sessions`, which `pageRoutePath("/settings/user/security", "/sessions")` spells out. A plain `Controller` a module mounts for its frontend sits under `/api/<module>/` instead, or under a prefix it declares (`backendProxies`, below).
180
-
181
- `GetPermissionId(target)` returns the permission id recorded for a page controller class or a `Component`, or `undefined` when the target was never registered. A child component resolves to its full positional path. When the same component instance is mounted more than once, every position registers a permission but the instance resolves to its first registered position, because the instance alone cannot identify which mount the caller means.
182
-
183
- ```typescript
184
- import { GetPermissionId } from "@antelopejs/interface-dms/page";
185
-
186
- const pagePermission = GetPermissionId(InvoicesPage); // "pages.billing.invoices"
187
- ```
188
-
189
- ## Declare a Module
190
-
191
- A module is a self-contained feature area with its own root in the sidebar. `RegisterModule(info)` creates that root under `modulesCategory` and returns its `CategoryInfo`.
192
-
193
- ```typescript
194
- import { RegisterModule } from "@antelopejs/interface-dms/page";
195
-
196
- RegisterModule({
197
- id: "automation",
198
- title: "$dms_automation.module.title",
199
- description: "$dms_automation.module.description",
200
- icon: "i-ph-flow-arrow",
201
- defaultCategory: { displayName: "$dms_automation.module.pages", order: 0 },
202
- });
203
- ```
204
-
205
- Call `RegisterModule` before any page that references the module id: a page resolves its module synchronously inside the `PageController` call of its class declaration, before `@RegisterPage()` runs, and an unknown module id throws. Registering the same id twice throws as well.
206
-
207
- ### `ModuleInfo`
208
-
209
- | Option | Type | Description |
210
- | ------ | ---- | ----------- |
211
- | `id` | `string` | Module id. Also the URL segment under `/modules`. |
212
- | `title` | `string` | Display name of the module root. |
213
- | `description` | `string` | Description carried on the module root. |
214
- | `icon` | `string` | Icon name. |
215
- | `landingPage` | `string` | Id of the page the module opens on; `/modules/<id>` redirects to it, query and hash kept. Defaults to the first page by `order`. |
216
- | `catalogCategory` | `string` | Category the module is filed under in the module catalog (`"Content"`, `"Developer"`…), may be a `$` i18n key. The Modules page and the store both file the module under it; without one, a store module takes the category of the store catalog, and any other goes under "Other". Unrelated to menu categories (`MenuOptions.category`, `defaultCategory`). Formerly `category`. |
217
- | `defaultCategory` | `{ displayName?, icon?, order?, urlSlug? }` | Groups the module's pages that declare `module` without an explicit `category`. Without it, those pages attach directly to the module root. |
218
-
219
- The generated default category uses `urlSlug` `"pages"` unless you override it, and falls back to the label `"$menu.section.pages"` when `displayName` is empty or omitted.
220
-
221
- ### Attaching Pages to a Module
222
-
223
- Set `module` on the page options. `category` stays available to group pages inside the module:
224
-
225
- ```typescript
226
- import {
227
- PageController,
228
- RegisterPage,
229
- } from "@antelopejs/interface-dms/page";
230
- import "./module"; // ensures RegisterModule ran first
231
-
232
- @RegisterPage()
233
- export class OverviewPage extends PageController("overview", {
234
- displayName: "$dms_automation.overview.title",
235
- icon: "i-ph-gauge",
236
- module: "automation",
237
- order: 0,
238
- }) {}
239
- ```
240
-
241
- When a page declares both `module` and an explicit `category`, the category must be a descendant of that module's root; otherwise registration throws.
242
-
243
- ### Module URLs and Detection
244
-
245
- `MODULE_URL_PREFIX` is `"/modules"`. Module pages resolve under `/modules/<moduleId>/...` because the module root's `urlSlug` is the module id and its parent is `modulesCategory`. Their ids follow the same chain: the module root's `fullId` is `modules.<moduleId>`, and a page in it is `modules.<moduleId>.<pageId>`.
246
-
247
- `isInsideModule(entry)` answers whether an entry belongs to a module. It accepts anything carrying `module`, `category` and `isModuleRoot` — a `CategoryInfo` or `PageInfo` qualifies — and walks the category chain looking for a module root.
248
-
249
- ```typescript
250
- import { isInsideModule } from "@antelopejs/interface-dms/page";
251
-
252
- if (isInsideModule(pageInfo)) {
253
- // module pages are gated to platform owners
254
- }
255
- ```
256
-
257
- Permissions belonging to a module entry are marked module-scoped instead of being registered in the grantable permission tree. [Permissions](/docs/interfaces/core/permissions) covers what that implies.
258
-
259
- ## Contribute Menu Entries at Request Time
260
-
261
- Everything above declares the menu at import time. A dynamic menu provider adds entries resolved per request instead — one entry per project, per workspace, per whatever the tenant happens to own.
262
-
263
- ```typescript
264
- import { RegisterDynamicMenuProvider } from "@antelopejs/interface-dms/page";
265
-
266
- RegisterDynamicMenuProvider("pages.projects", async (user, tenantId) => {
267
- const projects = await listProjects(tenantId);
268
- return projects.map((project, index) => ({
269
- id: project.slug,
270
- displayName: project.name,
271
- fullSlug: "/projects/:projectId",
272
- params: { projectId: project.id },
273
- icon: "i-ph-folder",
274
- order: index,
275
- }));
276
- });
277
- ```
278
-
279
- ```typescript
280
- function RegisterDynamicMenuProvider(
281
- categoryFullId: string,
282
- resolver: DynamicMenuResolver,
283
- ): () => void;
284
-
285
- type DynamicMenuResolver = (
286
- user: User | undefined,
287
- tenantId: string,
288
- permissions: Set<string>,
289
- ) => MaybePromise<DynamicMenuItem[]>;
290
- ```
291
-
292
- A registration is a `DynamicMenuProviderInfo` — the `categoryFullId` and the `resolver`, paired. The resolver runs during `/sitelayout`, and its entries are grafted onto that request's copy of the tree only — the global navigation tree is never touched, so one tenant's entries can never reach another's site layout. Dynamic and static children of the category sort together by `order`.
179
+ `RootPageController(id, options, layout?)` builds a page controller at the root of the tree. Its options are `MenuOptions` without `category` and `module`, plus an optional `isModuleRoot` boolean. It requires no parent, so it suits a top-level landing screen.
293
180
 
294
- `permissions` is the caller's effective permission set as the parent category sees it: empty while a tenant access gate denies the tenant, unless the category is flagged `bypassTenantAccessGate`, in which case the real set is kept. `user` is `undefined` for an unauthenticated request.
181
+ ## Identifiers a page carries
295
182
 
296
- A resolver that throws is skipped with a warning and the rest of the menu still resolves. The registration is removed when the registering module unloads; the returned function drops it earlier, for the rare case a module stops providing while staying loaded.
183
+ `Category`, `PageController` and `RootPageController` compute two identifiers from the entry's id and its parent chain:
297
184
 
298
- ### `DynamicMenuItem`
185
+ - `fullId` is the chain of ids joined by dots, from the root category (`pages.crm.contacts`). It is the page's permission id and the key its realtime topics register under.
186
+ - `fullSlug` is the chain of URL slugs (`/crm/contacts`).
299
187
 
300
- An item is not a page. It links to an already registered page, which is why one page — typically one whose slug has `:name` segments, or one with `validation.requiredQueryParams` — can back any number of entries.
188
+ `CategoryInfo` extends `MenuOptions` without `parent`. It redeclares `category` as a required `CategoryInfo | ControllerClass | undefined` that holds the resolved parent, and adds `id`, `fullId`, `fullSlug`, an optional `isModuleRoot`, and an optional `urlTransparent`. `urlTransparent` is `true` when the category adds no URL segment of its own, as `urlSlug: "/"` produces. Such a category is a section heading, not a route, and any number of them may sit under one parent.
301
189
 
302
- `params` fills the `:name` segments of `fullSlug`, URL-encoding each value, and the entry links to the result: `fullSlug: "/projects/:projectId"` with `params: { projectId: "acme" }` links to `/projects/acme`. The values must fill every segment and nothing else. An entry that leaves a segment empty, or names a parameter the slug does not have, is skipped with a warning rather than linking to a literal `:projectId`.
303
-
304
- | Field | Type | Description |
305
- | ----- | ---- | ----------- |
306
- | `id` | `string` | Unique within the provider's category, and becomes the entry's `fullId` suffix. Dots separate `fullId` levels, so an id containing one is skipped with a warning. Required. |
307
- | `displayName` | `string` | Entry label. Required. |
308
- | `fullSlug` | `string` | Full slug of the page the entry links to, `:name` segments included. Required. |
309
- | `params` | `MenuItemParams` | Values for the `:name` segments of `fullSlug`. |
310
- | `query` | `MenuItemQuery` | Query parameters narrowing that page. |
311
- | `description` | `string` | Longer description. |
312
- | `icon` | `string` | Icon name. |
313
- | `order` | `number` | Sort order among the category's children, dynamic and static alike. |
314
- | `variant` | `MenuItemVariant` | Semantic look. |
315
- | `status` | `MenuItemStatus` | Semantic state dot. |
316
- | `permissionId` | `string` | Permission required to see the entry, as a raw id. Unset means always visible. |
190
+ `PageInfo` extends `CategoryInfo` with `layoutUrl` and an optional `parentSlug`. `layoutUrl` is the URL the page's component tree is served from: `/dms/pages`, then the page's `fullSlug`, then `/pagelayout`. `parentSlug` is the full slug of the page declared as `parent`.
317
191
 
318
- An entry is listed only when the caller can access the page it points at, so it never offers a link its target would refuse.
192
+ A page controller serves its routes under `PAGE_ROUTE_PREFIX` (`/dms/pages`), not at the page's own path. The frontend server answers the page's own path with the page, and proxies only the backend prefixes declared to it, which are `/api/**` and `/dms/**` for the DMS. `@Get("/sessions")` on the page `/settings/user/security` is `GET /dms/pages/settings/user/security/sessions`, which `pageRoutePath("/settings/user/security", "/sessions")` returns. A plain `Controller` that a module mounts for its frontend sits under `/api/<module>/` instead, or under a prefix it declares in [`backendProxies`](/docs/interfaces/core/frontend-modules-and-request-helpers#addfrontendmoduleoptions).
319
193
 
320
- `permissionId` is a raw id here, unlike `MenuOptions.permission`, which takes a whole permission or an `Action`. It is matched against the permission set the caller's browser actually receives, the way the browser matches it — the wildcard or a literal grant, never a `defaultGranted` fallback — so the sidebar never offers an entry whose controls would be inert. While a gate denies the tenant that set holds only the surfaces flagged `bypassTenantAccessGate`, so a recovery entry declares its permission under its target page, as in `<recoveryPageFullId>.settle`.
194
+ `GetPermissionId(target)` returns the permission id recorded for a page controller class, a `Component`, or a child position, or `undefined` when the target never registered. A child component resolves to its full positional path. When one component instance is mounted more than once, every position registers a permission, but the instance alone resolves to its first registered position. `GetComponentPermissionIds(component)` returns the ids of every live position of a reusable component.
321
195
 
322
- ### `NotifyMenuChanged`
196
+ ```typescript fragment
197
+ import { GetPermissionId } from "@antelopejs/interface-dms/page";
323
198
 
324
- ```typescript
325
- const NotifyMenuChanged: (tenantId?: string) => void;
199
+ const pagePermission = GetPermissionId(InvoicesPage);
326
200
  ```
327
201
 
328
- Call it when the data behind a provider changes — a project created or deleted — so connected clients resync. The signal carries no data: it is an invalidation, and each client re-fetches its own `/sitelayout`, resolved against its own permissions.
202
+ For the page declared above, `pagePermission` is `"pages.billing.invoices"`. `GetRegisteredPageIds()` returns the ids of every registered page, sorted.
329
203
 
330
- Pass the `tenantId` whose data changed so only that tenant's sessions re-fetch. Omitting it invalidates the menu for every connected session, which is meant for changes that are not tenant-scoped.
204
+ ## Extend a page from another module
331
205
 
332
- ## Extend a Page from Another Module
333
-
334
- `@RegisterPageExtension(targetPageId)` injects components into a page owned by someone else. The decorated class is not a page: every static component field on it becomes a component of the target page, keyed by its field name.
206
+ `@RegisterPageExtension(targetPageId)` injects components into a page that another module owns. The decorated class is not a page. Every static component field on it becomes a component of the target page, keyed by its field name.
335
207
 
336
208
  ```typescript
337
209
  import { RegisterPageExtension } from "@antelopejs/interface-dms/page";
@@ -345,17 +217,22 @@ export class SeatQuotaExtension {
345
217
  }
346
218
  ```
347
219
 
348
- The target is named by its page id — the `fullId` the page registered under, which is also its permission id: the dotted path of its categories followed by its own id (`settings.workspace.members` for the DMS members page). Nothing of the owning module is imported, so an extending module depends on `@antelopejs/interface-dms` alone and never owns another module's registrations. `GetRegisteredPageIds()` lists the ids currently registered, and `GetPermissionId(ThePageClass)` returns one from inside the module that owns the page.
220
+ The target is named by its page id, the `fullId` it registered under, which is also its permission id. The DMS members page is `settings.workspace.members`. The extending module imports nothing from the owning module, so it depends on `@antelopejs/interface-dms` alone. `GetRegisteredPageIds()` lists the ids, and `GetPermissionId(ThePageClass)` returns one inside the owning module.
349
221
 
350
- Because the field name is the component key, an injected component is permissioned as `<target fullId>.<field name>` in the target page's own permission tree — exactly like a component the target declares itself. The target's own fields and every extension of it share that one namespace.
222
+ The field name is the component key, so an injected component's permission is `<target fullId>.<field name>` in the target page's own permission tree, like a component the target declares itself. The target's fields and those of every extension share that namespace.
351
223
 
352
- Placement is declared on the component with `.before(anchor)` / `.after(anchor)`, where the anchor is a component key of the target page — `"table"` for a static field of the page, `"content.tasks"` for a position nested under one. Without one, the component is appended after the page's own components. When several land at the same spot `.order(n)` decides, and equal orders fall back to the extension class name and then to declaration order — never to module start order. [Components](/docs/interfaces/core/components) covers those three builder methods.
224
+ `.before(anchor)` and `.after(anchor)` on the component set its placement. The anchor is a component key of the target page: `"table"` for a static field, or `"content.tasks"` for a nested position. Without an anchor, the component goes after the page's own components. When several components land at the same spot, `.order(n)` decides, and equal orders fall back to the extension class name and then to declaration order, never to module start order. [Components](/docs/interfaces/core/components) covers those builder methods.
353
225
 
354
- The injection applies as soon as the target page has finished registering — it is held until then whichever module starts first, so anchors always resolve against the page's complete component set — and is removed when the extending module stops. Its components are prepared as the extending module — whichever module's registration triggers it, and again each time the target page registers anew — so what they register (their permissions, native upload fields, form sub-pages, realtime topics) belongs to the extending module: it survives a reload of the DMS or of the target's module, and goes away with the extending module. An extension class declaring no component field is a warning, not an error, and injects nothing; an empty target id throws.
226
+ The injection applies once the target page finishes registering, whichever module starts first, so anchors resolve against the page's complete component set. It is removed when the extending module stops. The components are prepared as the extending module, each time the target page registers. What they register, such as permissions, native upload fields, form sub-pages and realtime topics, belongs to the extending module. It survives a reload of the DMS or of the target's module and goes away with the extending module.
355
227
 
356
- Nothing about the target can be checked when the decorator runs, so everything is checked when the injection applies: a page id no module registers keeps the extension pending and is reported as a warning once every module has started, while a key the page (or another extension) already uses and an anchor the page does not declare are reported at error level — with the extension name, the target id and the offending key — and that extension alone is skipped.
228
+ Nothing about the target can be checked when the decorator runs, so the checks happen when the injection applies:
357
229
 
358
- ```typescript
230
+ - An empty target id throws at the decorator.
231
+ - An extension class with no component field logs a warning and injects nothing.
232
+ - A page id that no module registers keeps the extension pending, with a warning once every module has started.
233
+ - A key that the page or another extension already uses, or an anchor that the page does not declare, is reported at error level with the extension name, the target id and the key. That extension alone is skipped.
234
+
235
+ ```typescript fragment
359
236
  interface PageExtensionInfo {
360
237
  extensionName: string;
361
238
  targetFullId: string;
@@ -366,46 +243,48 @@ interface PageExtensionComponent {
366
243
  key: string;
367
244
  component: Component;
368
245
  side: PlacementSide;
369
- anchorKey?: string;
246
+ anchorPath?: readonly string[];
370
247
  order: number;
371
248
  }
372
249
  ```
373
250
 
374
- `PageExtensionInfo` is the registration handle: it is the object that gets unregistered when the extending module stops. `key` is the static field name the component was declared under, and also the last segment of its permission id on the target page.
251
+ `PageExtensionInfo` is the registration handle that is unregistered when the extending module stops. `key` is the static field name, and also the last segment of the component's permission id on the target page. `anchorPath` is the anchor split into its dotted segments.
375
252
 
376
- ## Page Layouts
253
+ ## Page layouts
377
254
 
378
255
  A page layout is the serialized description of the page's components:
379
256
 
380
- ```typescript
257
+ ```typescript fragment
381
258
  interface PageLayout<T = unknown> {
382
259
  components: Record<string, ComponentInfoSerialized<T>>;
383
260
  layout?: ComponentInfo<T>;
261
+ allComponentsHidden?: true;
384
262
  }
385
263
  ```
386
264
 
387
- `components` is keyed by the static field name on the page class; each value is the serialized form of the component ([Components](/docs/interfaces/core/components)). `layout` is the layout component passed as `PageController`'s third argument.
265
+ `components` is keyed by the static field name on the page class, and each value is the serialized component ([Components](/docs/interfaces/core/components)). `layout` is the layout component. `allComponentsHidden` is set when the page declares components and the caller may see none of them, so the browser can say so instead of drawing a blank page.
388
266
 
389
267
  Registering a page installs a `PageLayoutHandler` for its `fullSlug`:
390
268
 
391
- ```typescript
269
+ ```typescript fragment
392
270
  interface PageLayoutRequest {
393
271
  user: User | undefined;
394
272
  memberModel: TenantMemberModel;
395
273
  roleModel: RoleModel;
396
274
  tenantId: string;
397
- /** The page URL's query, for header-button availability. */
398
275
  query?: Readonly<Record<string, string>>;
399
- /** The page URL's route parameters (`:id`), likewise. */
400
276
  params?: Readonly<Record<string, string>>;
401
- /** Passed to the permissions resolvers: `{ preview }` when a role preview reads the layout. */
402
277
  resolverContext?: PermissionsResolverContext;
403
278
  }
404
279
 
405
280
  type PageLayoutHandler = (request: PageLayoutRequest) => Promise<PageLayout>;
406
281
  ```
407
282
 
408
- The handler drops the components the caller has no permission for and applies each component's own filter. Native upload tokens are bound to the exact page/component mount during page registration, not stamped by this handler; upload and metadata requests still check live authorization ([File storage](/docs/components/file-storage)). A page with `publicAccess`, `authOnly` or `noComponentPermissions` skips the filtering step and returns every component. Look a handler up with `GetPageLayoutBySlug(slug)`, which returns `undefined` for an unknown slug:
283
+ `query` and `params` are the page URL's query and route parameters, handed to header-button availability. `resolverContext` goes to the permissions resolvers, with `preview` set when the layout is served for a role preview.
284
+
285
+ The handler drops the components the caller has no permission for and applies each component's own filter. A page with `publicAccess`, `authOnly` or `noComponentPermissions` skips the permission step and returns every component. Native upload tokens are bound to the exact page and component mount at page registration, not by this handler, and upload and metadata requests still check live authorization ([File storage](/docs/components/file-storage)).
286
+
287
+ `GetPageLayoutBySlug(slug)` returns the handler, or `undefined` for an unknown slug:
409
288
 
410
289
  ```typescript
411
290
  import { GetModel } from "@antelopejs/interface-database-decorators";
@@ -434,40 +313,43 @@ async function renderLayout(
434
313
  }
435
314
  ```
436
315
 
437
- `ClearPageLayoutBySlug(slug)` drops the handler again. A page unregisters through it: the handler is reachable by slug from `/dms/pagelayout`, so leaving it behind keeps serving the layout of a page that is already gone from the registry, the navigation tree and its own route — an unloaded module's screens would still answer.
316
+ `ClearPageLayoutBySlug(slug)` drops the handler. A page unregisters through it, because `/dms/pagelayout` reaches the handler by slug. A handler left behind would keep serving the layout of a page already gone from the registry, the navigation tree and its own route.
438
317
 
439
318
  ### `PageMetadata`
440
319
 
441
- `PageMetadata` is the per-controller record the decorator fills in. `GetMetadata(controllerClass, PageMetadata)` returns it. It holds `pageInfo`, `layout` and `components`, and exposes:
320
+ `PageMetadata` is the record the decorator fills in for each controller. `GetMetadata(controllerClass, PageMetadata)` returns it. It holds `pageInfo`, `layout` and `components`, and exposes:
442
321
 
443
322
  | Member | Description |
444
323
  | ------ | ----------- |
445
- | `SetInfo(id, fullSlug, menuOptions, pageLayout?)` | Compute and store the `PageInfo` and the layout. |
446
- | `SetComponent(id, component)` | Attach a component under a key. |
447
- | `inherit(parent)` | Merge another `PageMetadata`'s info, layout and components into this one. |
448
- | `Register()` | Register the page: permissions, layout handler, `/pagelayout` route and auth decorators. |
449
- | `target` | The controller class the metadata belongs to. |
324
+ | `SetInfo(id, fullSlug, menuOptions, pageLayout?)` | Computes and stores the `PageInfo` and the layout. |
325
+ | `SetComponent(id, component)` | Attaches a component under a key. |
326
+ | `inherit(parent)` | Merges another `PageMetadata`'s info, layout and components into this one. |
327
+ | `Register()` | Registers the page: permissions, layout handler, `/pagelayout` route and auth decorators. |
328
+ | `ComponentPermissionIds(component)` | Returns the permission ids of every position the component holds on this page. |
329
+ | `target` | The controller class the record belongs to. |
330
+
331
+ `@RegisterPage()` calls `SetComponent` for each static component and then `Register()`. Use `PageMetadata` directly only to generate pages programmatically.
450
332
 
451
- `@RegisterPage()` does this for you: it scans the class's own static properties, records every `Component` it finds under its property name, and calls `Register()`. Static properties whose name starts with `_` are skipped, which is how you keep a helper value on the class without turning it into a layout entry. Reach for `PageMetadata` directly only when you generate pages programmatically.
333
+ Registration also runs each component's `onCreated` callback with this record. It registers a permission for each component and each component action, unless the page sets `noComponentPermissions` or `authOnly`, or sits inside a module ([Permissions](/docs/interfaces/core/permissions)).
452
334
 
453
- Registration also runs each component's `onCreated` callback with this record, and registers a permission per component and per component action unless the page opts out ([Permissions](/docs/interfaces/core/permissions)).
335
+ A declaration that the page cannot honour throws `PageDeclarationConflictError` and the page fails to register. `FormPageRouteConflictError` extends it for two page-mode table views that register the same form sub-page.
454
336
 
455
- ## Page Access
337
+ ## Page access
456
338
 
457
- Registering a page installs an auth decorator on the controller, chosen from the options:
339
+ Registering a page installs an auth decorator on the controller's `user` property, chosen from the options:
458
340
 
459
341
  | Options | Decorator installed |
460
342
  | ------- | ------------------- |
461
- | `publicAccess: true` | none — the page is served to anonymous callers |
462
- | `authOnly: true` | `AuthUser` from `@antelopejs/interface-dms/auth`: a logged-in user, no permission check |
463
- | page under `/modules/` | `AuthOwnerOnly` from `@antelopejs/interface-dms/auth`: module pages are platform-owner-only |
464
- | default | `AuthUserWithPermission(pageClass)` — the page's own permission ([Permissions](/docs/interfaces/core/permissions)) |
343
+ | `publicAccess: true` | None. The page is served to anonymous callers. |
344
+ | `authOnly: true` | `AuthUser` from `@antelopejs/interface-dms/auth`. A signed-in user, no permission check. |
345
+ | page whose `fullSlug` starts with `/modules/` | `AuthOwnerOnly` from `@antelopejs/interface-dms/auth`. Module pages are for platform owners only. |
346
+ | default | `AuthUserWithPermission(pageClass, { bypassTenantAccessGate })` with the page's own permission ([Permissions](/docs/interfaces/core/permissions#authuserwithpermission)). |
465
347
 
466
- On a page that is neither `publicAccess` nor `authOnly`, registration also binds `roleModel`, `memberModel` and `tenantId` properties on the controller, scoped to the request's tenant ([Tenancy](/docs/interfaces/core/tenancy)). Routes you declare on the page class inherit that gating: a route under a default page already requires the page's permission.
348
+ On a page that is neither `publicAccess` nor `authOnly`, registration also binds `roleModel`, `memberModel` and `tenantId` properties on the controller, scoped to the request's tenant ([Tenancy](/docs/interfaces/core/tenancy)). Routes declared on the page class inherit that gating, so a route on a default page already requires the page's permission.
467
349
 
468
- Declaring the injected property on the class is enough to read it in a handler — the provider runs per request whether or not the handler reads it:
350
+ Declare an injected property on the class to read it in a handler. The provider runs on every request, whether or not the handler reads it:
469
351
 
470
- ```typescript
352
+ ```typescript fragment
471
353
  import { Get } from "@antelopejs/interface-api";
472
354
  import {
473
355
  PageController,
@@ -490,11 +372,11 @@ export class MyContactsPage extends PageController("mine", {
490
372
  }
491
373
  ```
492
374
 
493
- ## Page Setup Functions
375
+ ## Page setup functions
494
376
 
495
- `setupId` names a function that runs while the page is mounted. The backend only carries the id; the function itself is registered on the frontend side and receives a `PageSetupContext`.
377
+ `setupId` names a function that runs while the page is mounted. The backend carries only the id. The frontend registers the function, runs it when the page mounts, and passes it a `PageSetupContext`.
496
378
 
497
- ```typescript
379
+ ```typescript fragment
498
380
  type PageSetupFunction = (
499
381
  context: PageSetupContext,
500
382
  ) => MaybePromise<PageSetupCleanup | void>;
@@ -502,14 +384,14 @@ type PageSetupFunction = (
502
384
 
503
385
  | Member of `PageSetupContext` | Description |
504
386
  | ---------------------------- | ----------- |
505
- | `pageInfo` | The `PageInfo` of the page being mounted. |
387
+ | `pageInfo` | The `PageInfo` of the mounted page. |
506
388
  | `permissions` | `Set<string>` of the caller's effective permission ids. |
507
- | `on(component, event, handler)` | Subscribe to a component event. Returns an unsubscribe function. |
508
- | `emit(component, event, data?)` | Dispatch an event on the shared event bus. |
389
+ | `on(component, event, handler)` | Subscribes to a component event. Returns an unsubscribe function. |
390
+ | `emit(component, event, data?)` | Dispatches an event on the shared event bus. |
509
391
 
510
- `PageSetupCleanup` is `() => void`, returned by the setup function to release what it allocated when the page unmounts. `MaybePromise<T>` is `T | Promise<T>`, re-exported here from `@antelopejs/interface-dms/types`.
392
+ `PageSetupCleanup` is `() => void`. The setup function returns it to release what it allocated, and the frontend calls it when the page unmounts. `MaybePromise<T>` is `T | Promise<T>`, re-exported from `@antelopejs/interface-dms/types`.
511
393
 
512
- ```typescript
394
+ ```typescript fragment
513
395
  import {
514
396
  PageController,
515
397
  RegisterPage,
@@ -524,27 +406,11 @@ export class LifecyclePage extends PageController("lifecycle", {
524
406
  }) {}
525
407
  ```
526
408
 
527
- ## Ship an Inertia/Vue Frontend Module
528
-
529
- `AddFrontendModule(config)` registers a frontend module your module ships. Call it from your module's `construct()`.
409
+ ## Frontend modules and request helpers
530
410
 
531
- ```typescript
532
- import path from "node:path";
533
- import { AddFrontendModule } from "@antelopejs/interface-dms/page";
534
-
535
- export async function construct(): Promise<void> {
536
- await AddFrontendModule({
537
- name: "@acme/dms-billing",
538
- sourcePath: path.join(__dirname, "../frontend-vue"),
539
- renderer: { name: "vue", version: "3" },
540
- configKey: "dmsBilling",
541
- priority: 0,
542
- options: { prefix: "Billing" },
543
- });
544
- }
545
- ```
411
+ [Frontend modules and request helpers](/docs/interfaces/core/frontend-modules-and-request-helpers) describes the functions a module's backend calls outside page declarations.
546
412
 
547
- The frontend reads the project, not the backend's registrations: `ajs dms dev`, `ajs dms build` and `ajs dms start` call the module's `frontendModule` function, named as `dmsFrontend` in the module's `package.json` (`"dmsFrontend": "dist/frontend-module.js"`). Keep the registration in that pure function of the module's config, and pass its result to `AddFrontendModule`; a module that only calls `AddFrontendModule` is left out of the frontend, in development as in production.
413
+ The frontend reads the project, not the backend's registrations. `ajs dms dev`, `ajs dms build` and `ajs dms start` call the module's `frontendModule` function, which the module's `package.json` names as `dmsFrontend` (`"dmsFrontend": "dist/frontend-module.js"`). Keep the registration in that pure function of the module's config, and pass its result to `AddFrontendModule`. A module that only calls `AddFrontendModule` is left out of the frontend, in development and in production.
548
414
 
549
415
  ```typescript
550
416
  import path from "node:path";
@@ -567,113 +433,20 @@ export const frontendModule: FrontendModuleFunction<BillingConfig> = (
567
433
 
568
434
  ### `AddFrontendModuleOptions`
569
435
 
570
- | Option | Type | Description |
571
- | ------ | ---- | ----------- |
572
- | `name` | `string` | Logical frontend module name. Registry identity also includes renderer name and major version. Required. |
573
- | `sourcePath` | `string` | Absolute path to the frontend module directory. Required. |
574
- | `renderer` | `FrontendRendererMetadata` | Framework the module is written for, as a name and a version compared by major only. Use `{ name: "vue", version: "3" }` for the Inertia/Vue frontend. The loader releases the module runs on are declared in the `package.json` of `sourcePath` instead ([Frontend layer](/docs/building/frontend-layer#declaring-the-loader-releases-a-module-supports)). |
575
- | `options` | `Record<string, any>` | Values handed to the layer's configuration. Defaults to `{}`. |
576
- | `privateOptions` | `Record<string, any>` | A second set of values handed to the layer, kept separate from `options`. Defaults to `{}`. |
577
- | `priority` | `number` | Ordering among the registered layers. Defaults to `0`. |
578
- | `configKey` | `string` | Configuration key the layer is published under. |
579
- | `authEstablishEndpoints` | `string[]` | Backend routes (`/api/…`) the frontend server may open a session from on the module's behalf (its `/auth/establish` route). |
580
- | `publicEndpoints` | `FrontendPublicEndpoint[]` | Backend routes the module serves to callers outside the dashboard — a provider's webhook. See below. |
581
- | `backendProxies` | `FrontendBackendProxy[]` | Backend prefixes outside `/api/**` and `/dms/**` the browser reaches through the frontend server (`{ path: "/media/**", auth: "none" }`); `auth` is `"session"` (the default, the session's token added server-side) or `"none"`. A path is `/x`, `/x/*` or `/x/**`, its first segment literal; a malformed entry fails the registration. |
582
-
583
- #### Public endpoints
584
-
585
- The frontend server's proxy asks every state-changing request for a same-site `Origin` and forwards it with the signed-in user's session, relaying only a few headers. A webhook a mail or payment provider calls through the dashboard's public URL fails both checks. Declare such a route public:
586
-
587
- ```typescript
588
- await AddFrontendModule({
589
- name: "@acme/dms-mailing",
590
- sourcePath: path.join(__dirname, "../frontend-vue"),
591
- renderer: { name: "vue", version: "3" },
592
- publicEndpoints: [
593
- {
594
- path: "/api/mailing/events/*/*",
595
- methods: ["POST"],
596
- headers: ["x-mailing-webhook-secret"],
597
- },
598
- ],
599
- });
600
- ```
601
-
602
- A request to a declared route needs no `Origin`, carries the declared headers, and is **never sent the session**, even from a signed-in browser: a cross-site form posting to it acts as nobody, and the route authenticates its caller itself (a shared secret, a signature). `path` is absolute under `/api/`, its first segment literal (the module's own prefix, never `*`), each later `*` matching exactly one segment; `methods` defaults to `["POST"]`; `headers` names the module's own `x-` headers, those a proxy or the DMS gives a meaning to (`x-forwarded-*`, `x-real-ip`, `x-dms-*`, `x-inertia*`, method overrides) being refused. A malformed entry fails the registration. The declarations reach the frontend server through the manifest it is built from (an authenticated build only), and its `DMS_PUBLIC_ENDPOINTS` adds routes no module declares. It needs `@antelopejs/dms-frontend` with public endpoint support; an older one ignores the field.
603
-
604
- Within one logical name, renderer, and major version, the highest-priority
605
- registration wins. Equal-priority registrations use the latest registration.
606
- The manifest and archive endpoints select one renderer with the paired
607
- `renderer` and `rendererVersion` query parameters. The version is matched by
608
- major version; unsupported selections return 404 rather than mixing renderer
609
- sources. Omitting both parameters remains an alias for Vue 3 clients.
610
-
611
- ## Stamp Upload Tokens
612
-
613
- `StampUploadFieldTokens(options)` takes a component's serialized options and returns them with a signed upload token stamped into every file and image field — the component's own fields, and the forms it embeds (a relation field's inline add form, a table view's `new` / `edit` / `view` forms) — binding each field to the storage and path it declares so a client cannot presign an upload elsewhere. The builders that carry upload fields attach it through `transformOptions`, and page registration stamps the authoritative declaration, so pages you declare with `@RegisterPage()` are covered already. Call it yourself only when you serialize and serve components outside that path: a token produced outside a registered page stays unbound and cannot authorize an upload.
614
-
615
- ```typescript
616
- import { StampUploadFieldTokens } from "@antelopejs/interface-dms/uploads";
617
-
618
- const components = await StampUploadFieldTokens(layout.components);
619
- ```
620
-
621
- Options holding no upload field come back untouched; otherwise the return value is a deep clone, since serialization runs again on a hot reload and each pass must start from the untransformed declaration.
622
-
623
- ## Public URLs
624
-
625
- `GetApiBaseUrl()` and `GetClientBaseUrl()` answer the public origins of the DMS API and of the dashboard, from the DMS configuration (`apiBaseUrl`, `clientBaseUrl`); under `ajs dms dev` they follow the dev server's origins. Build an absolute link on them rather than on the request's headers: behind a reverse proxy that forwards no `X-Forwarded-Host`, the request only knows the internal host.
626
-
627
- ```typescript
628
- import { GetApiBaseUrl } from "@antelopejs/interface-dms/api-base-url";
629
- import { GetClientBaseUrl } from "@antelopejs/interface-dms/client-base-url";
630
-
631
- const apiBase = await GetApiBaseUrl(); // "https://api.example.com"
632
- const downloadUrl = `${apiBase}/i18n/site/fr.json`;
633
- const inviteUrl = `${await GetClientBaseUrl()}/auth/signup`;
634
- ```
635
-
636
- Both return `string | undefined`: `undefined` only when the configuration clears the value.
637
-
638
- ## Client address
639
-
640
- `getClientIp(context)` answers the address a request came from, resolved the way the DMS resolves the one it records on a session: behind the `auth.trustedProxies` reverse proxies of its configuration ([Client address behind proxies](/docs/building/configuration#client-address-behind-proxies)), each trusted hop vouching for the `x-forwarded-for` entry it appended and nothing left of it, and an IPv4-mapped address (`::ffff:10.0.0.1`) written as plain IPv4. Key a rate limit or an anonymous visitor on it rather than on `x-forwarded-for` read by hand: the operator declares the proxies once, for the DMS and every module.
641
-
642
- ```typescript
643
- import { Context, Controller, Post, type RequestContext } from "@antelopejs/interface-api";
644
- import { getClientIp } from "@antelopejs/interface-dms/client-ip";
645
-
646
- export class CollectController extends Controller("/api/collect") {
647
- @Context()
648
- declare requestContext: RequestContext;
649
-
650
- @Post("/")
651
- async collect(): Promise<void> {
652
- const address = await getClientIp(this.requestContext); // "109.132.199.8"
653
- }
654
- }
655
- ```
656
-
657
- It answers an empty string when the request carries no address. `ResolveClientIp({ socketAddress, forwardedFor })` is the same resolution on values a module already holds, a request it queued for later say.
436
+ The options of `AddFrontendModule`, which registers the frontend a module ships, are listed in [`AddFrontendModuleOptions`](/docs/interfaces/core/frontend-modules-and-request-helpers#addfrontendmoduleoptions).
658
437
 
659
- ## `SaveComponentFiles`
438
+ ### Public URLs
660
439
 
661
- `SaveComponentFiles<T>(request: AttachmentSaveRequest, save: (promoted: Record<string, unknown>) => Promise<AttachmentSaveResult<T>>): Promise<T>` comes from `@antelopejs/interface-dms/attachments`. Call it from an authorized write route to validate native-file provenance, promote staging keys, and persist through `save`. The [file-storage guide](/docs/components/file-storage) covers the form/upload flow and storage authorization.
440
+ `GetApiBaseUrl()` and `GetClientBaseUrl()` are described in [Public URLs](/docs/interfaces/core/frontend-modules-and-request-helpers#public-urls).
662
441
 
663
- `AttachmentSaveRequest` contains:
442
+ ### Client address
664
443
 
665
- | Field | Type | Contract |
666
- | --- | --- | --- |
667
- | `context` | `RequestContext` | The authorized server request. |
668
- | `componentIds` | `string[]` | Trusted component identities, not ids supplied by the client. |
669
- | `fields` | `AttachmentField[]` | Server-declared attachment fields. |
670
- | `submitted` | `Record<string, unknown>` | The submitted values. |
671
- | `before` | `Record<string, unknown>` | Previously persisted values, loaded by the server. |
444
+ `getClientIp(context)` and `ResolveClientIp(source)` are described in [Client address](/docs/interfaces/core/frontend-modules-and-request-helpers#client-address).
672
445
 
673
- Each `AttachmentField` has `id` (matching the form's `attachmentField`), `key` (the document field), and `kind: "file" | "image"`. Optional `storage`, `visibility: "private" | "public"`, and `constraints: UploadConstraints` select the provider and upload policy; `UploadConstraints` comes from `@antelopejs/interface-file-storage`.
446
+ ### `SaveComponentFiles`
674
447
 
675
- The callback returns `AttachmentSaveResult<T>`: `{ document, result }`, where `document` contains the actual saved values used for cleanup, and `result` is returned to the caller. Removed native files are cleaned only after successful persistence; cleanup failures are logged. Failed saves leave promoted objects intact because another save may reference them. Cleanup is not reference-counted: callers must avoid unsafe sharing of file keys. Internal attachment validators and TableView save proxies are not consumer entry points.
448
+ `SaveComponentFiles` is described in [`SaveComponentFiles`](/docs/interfaces/core/frontend-modules-and-request-helpers#savecomponentfiles).
676
449
 
677
- ## Next Steps
450
+ ## Next steps
678
451
 
679
- [Components](/docs/interfaces/core/components) covers the builders that produce the values you assign to those static fields.
452
+ [Components](/docs/interfaces/core/components) covers the builders that produce the values you assign to those static fields. [Modules and dynamic menus](/docs/interfaces/core/modules-and-dynamic-menus) covers module roots and per-request menu entries.