@antelopejs/interface-dms 0.7.4 → 0.9.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 (239) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/dist/auth/index.d.ts +3 -3
  3. package/dist/auth/index.js +11 -9
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/auth/internal/jwt-auth-decorator.d.ts +20 -0
  6. package/dist/auth/internal/jwt-auth-decorator.js +51 -0
  7. package/dist/auth/internal/jwt-auth-decorator.js.map +1 -0
  8. package/dist/auth/internal/request-authenticators.d.ts +12 -0
  9. package/dist/auth/internal/request-authenticators.js +22 -0
  10. package/dist/auth/internal/request-authenticators.js.map +1 -1
  11. package/dist/base/activity-feed.d.ts +6 -6
  12. package/dist/base/block-registry/types.d.ts +16 -0
  13. package/dist/base/block-types.d.ts +1 -0
  14. package/dist/base/block-types.js +1 -0
  15. package/dist/base/block-types.js.map +1 -1
  16. package/dist/base/chart-card.d.ts +15 -0
  17. package/dist/base/chart-card.js +4 -0
  18. package/dist/base/chart-card.js.map +1 -1
  19. package/dist/base/chart-schemas.d.ts +26 -26
  20. package/dist/base/code-block.d.ts +3 -3
  21. package/dist/base/data-types/compare-types.js +19 -13
  22. package/dist/base/data-types/compare-types.js.map +1 -1
  23. package/dist/base/data-types/core.d.ts +11 -0
  24. package/dist/base/data-types/core.js +14 -1
  25. package/dist/base/data-types/core.js.map +1 -1
  26. package/dist/base/data-types/default-types.d.ts +112 -2
  27. package/dist/base/data-types/default-types.js +90 -19
  28. package/dist/base/data-types/default-types.js.map +1 -1
  29. package/dist/base/data-types/field-types.d.ts +10 -4
  30. package/dist/base/data-types/field-types.js +12 -2
  31. package/dist/base/data-types/field-types.js.map +1 -1
  32. package/dist/base/data-types/internal/compare-types.d.ts +3 -0
  33. package/dist/base/data-types/internal/compare-types.js +7 -0
  34. package/dist/base/data-types/internal/compare-types.js.map +1 -1
  35. package/dist/base/empty-state.d.ts +4 -4
  36. package/dist/base/form-block-schema.d.ts +59 -3
  37. package/dist/base/form-block-schema.js +12 -2
  38. package/dist/base/form-block-schema.js.map +1 -1
  39. package/dist/base/form-schema.d.ts +57 -5
  40. package/dist/base/form-schema.js +22 -0
  41. package/dist/base/form-schema.js.map +1 -1
  42. package/dist/base/form-types.d.ts +82 -13
  43. package/dist/base/form-types.js +12 -4
  44. package/dist/base/form-types.js.map +1 -1
  45. package/dist/base/guard.d.ts +6 -6
  46. package/dist/base/helpers/archive-mode-helpers.js +6 -2
  47. package/dist/base/helpers/archive-mode-helpers.js.map +1 -1
  48. package/dist/base/index.d.ts +1 -0
  49. package/dist/base/index.js +1 -0
  50. package/dist/base/index.js.map +1 -1
  51. package/dist/base/internal/form-schema.js +5 -0
  52. package/dist/base/internal/form-schema.js.map +1 -1
  53. package/dist/base/internal/layouts.d.ts +15 -3
  54. package/dist/base/key-value-list.d.ts +12 -9
  55. package/dist/base/key-value-list.js +1 -1
  56. package/dist/base/key-value-list.js.map +1 -1
  57. package/dist/base/kpi-card.d.ts +39 -5
  58. package/dist/base/kpi-card.js +18 -6
  59. package/dist/base/kpi-card.js.map +1 -1
  60. package/dist/base/layouts.d.ts +24 -1
  61. package/dist/base/layouts.js.map +1 -1
  62. package/dist/base/live-toggle.d.ts +11 -0
  63. package/dist/base/live-toggle.js +8 -1
  64. package/dist/base/live-toggle.js.map +1 -1
  65. package/dist/base/page-toolbar.d.ts +158 -0
  66. package/dist/base/page-toolbar.js +198 -0
  67. package/dist/base/page-toolbar.js.map +1 -0
  68. package/dist/base/period-selector.d.ts +9 -2
  69. package/dist/base/period-selector.js.map +1 -1
  70. package/dist/base/placeholder.d.ts +2 -2
  71. package/dist/base/section.d.ts +2 -2
  72. package/dist/base/stack.d.ts +2 -2
  73. package/dist/base/stat-group.d.ts +18 -6
  74. package/dist/base/stat-group.js +9 -0
  75. package/dist/base/stat-group.js.map +1 -1
  76. package/dist/base/table-view/column-display.d.ts +9 -5
  77. package/dist/base/table-view/column-display.js +1 -1
  78. package/dist/base/table-view/column-display.js.map +1 -1
  79. package/dist/base/table-view/data-functions.d.ts +26 -7
  80. package/dist/base/table-view/data-functions.js +10 -6
  81. package/dist/base/table-view/data-functions.js.map +1 -1
  82. package/dist/base/table-view/factory.js +24 -13
  83. package/dist/base/table-view/factory.js.map +1 -1
  84. package/dist/base/table-view/internal/auth.d.ts +2 -0
  85. package/dist/base/table-view/internal/auth.js +3 -1
  86. package/dist/base/table-view/internal/auth.js.map +1 -1
  87. package/dist/base/table-view/internal/column-conditions.d.ts +15 -0
  88. package/dist/base/table-view/internal/column-conditions.js +94 -0
  89. package/dist/base/table-view/internal/column-conditions.js.map +1 -0
  90. package/dist/base/table-view/internal/factory-helpers.d.ts +5 -0
  91. package/dist/base/table-view/internal/factory-helpers.js +55 -11
  92. package/dist/base/table-view/internal/factory-helpers.js.map +1 -1
  93. package/dist/base/table-view/internal/guards.js +19 -2
  94. package/dist/base/table-view/internal/guards.js.map +1 -1
  95. package/dist/base/table-view/internal/realtime.js +17 -0
  96. package/dist/base/table-view/internal/realtime.js.map +1 -1
  97. package/dist/base/table-view/internal/resource-form.js +1 -0
  98. package/dist/base/table-view/internal/resource-form.js.map +1 -1
  99. package/dist/base/table-view/internal/row-actions-schema.d.ts +296 -0
  100. package/dist/base/table-view/internal/row-actions-schema.js +62 -0
  101. package/dist/base/table-view/internal/row-actions-schema.js.map +1 -0
  102. package/dist/base/table-view/internal/validation.d.ts +22 -3
  103. package/dist/base/table-view/internal/validation.js +92 -2
  104. package/dist/base/table-view/internal/validation.js.map +1 -1
  105. package/dist/base/table-view/meta.d.ts +25 -1
  106. package/dist/base/table-view/meta.js +36 -4
  107. package/dist/base/table-view/meta.js.map +1 -1
  108. package/dist/base/table-view/options.d.ts +92 -16
  109. package/dist/base/table-view/options.js.map +1 -1
  110. package/dist/base/table-view/realtime.d.ts +8 -0
  111. package/dist/base/table-view/realtime.js.map +1 -1
  112. package/dist/base/table-view/routes.js +8 -4
  113. package/dist/base/table-view/routes.js.map +1 -1
  114. package/dist/base/table-view/schema.d.ts +36 -30
  115. package/dist/base/table-view/schema.js +11 -57
  116. package/dist/base/table-view/schema.js.map +1 -1
  117. package/dist/base/table-view/source.d.ts +1 -1
  118. package/dist/base/table-view/source.js +4 -1
  119. package/dist/base/table-view/source.js.map +1 -1
  120. package/dist/base/table-view.d.ts +2 -2
  121. package/dist/base/table-view.js.map +1 -1
  122. package/dist/base/top-list-card.d.ts +5 -5
  123. package/dist/base/types/action-target.d.ts +77 -20
  124. package/dist/base/types/confirm-dialog.d.ts +5 -1
  125. package/dist/base/types/custom-button.d.ts +5 -3
  126. package/dist/base/types/field-config.d.ts +4 -4
  127. package/dist/base/types/guards.d.ts +10 -0
  128. package/dist/base/types/row-action.d.ts +6 -1
  129. package/dist/base/types/size.d.ts +3 -1
  130. package/dist/client-ip.d.ts +6 -2
  131. package/dist/client-ip.js +4 -2
  132. package/dist/client-ip.js.map +1 -1
  133. package/dist/component.d.ts +14 -0
  134. package/dist/component.js.map +1 -1
  135. package/dist/data-controllers/members.d.ts +0 -3
  136. package/dist/data-controllers/members.js +30 -8
  137. package/dist/data-controllers/members.js.map +1 -1
  138. package/dist/db/internal/tenantLifecycle.model.js +5 -2
  139. package/dist/db/internal/tenantLifecycle.model.js.map +1 -1
  140. package/dist/guards.js +2 -3
  141. package/dist/guards.js.map +1 -1
  142. package/dist/hooks.d.ts +54 -10
  143. package/dist/hooks.js +12 -4
  144. package/dist/hooks.js.map +1 -1
  145. package/dist/index.d.ts +3 -3
  146. package/dist/index.js +3 -2
  147. package/dist/index.js.map +1 -1
  148. package/dist/internal/invite-membership.d.ts +9 -0
  149. package/dist/internal/invite-membership.js +25 -7
  150. package/dist/internal/invite-membership.js.map +1 -1
  151. package/dist/internal/member-changes.d.ts +24 -0
  152. package/dist/internal/member-changes.js +66 -0
  153. package/dist/internal/member-changes.js.map +1 -0
  154. package/dist/internal/tenant-ownership.d.ts +4 -1
  155. package/dist/internal/tenant-ownership.js +4 -10
  156. package/dist/internal/tenant-ownership.js.map +1 -1
  157. package/dist/invite-extensions/internal/form-slot.js +1 -1
  158. package/dist/invite-extensions/internal/form-slot.js.map +1 -1
  159. package/dist/invite-extensions.d.ts +0 -9
  160. package/dist/invite-extensions.js +1 -36
  161. package/dist/invite-extensions.js.map +1 -1
  162. package/dist/invite-resolution.js +6 -2
  163. package/dist/invite-resolution.js.map +1 -1
  164. package/dist/invites.js +36 -22
  165. package/dist/invites.js.map +1 -1
  166. package/dist/layout-banners.d.ts +40 -4
  167. package/dist/layout-banners.js +25 -3
  168. package/dist/layout-banners.js.map +1 -1
  169. package/dist/notifications/internal.d.ts +2 -2
  170. package/dist/notifications/internal.js.map +1 -1
  171. package/dist/notifications/sendable.d.ts +3 -2
  172. package/dist/notifications/sendable.js +2 -1
  173. package/dist/notifications/sendable.js.map +1 -1
  174. package/dist/notifications/types.d.ts +9 -0
  175. package/dist/page/controllers.d.ts +44 -0
  176. package/dist/page/controllers.js +14 -1
  177. package/dist/page/controllers.js.map +1 -1
  178. package/dist/page/internal/layout-filter.d.ts +10 -4
  179. package/dist/page/internal/layout-filter.js +27 -14
  180. package/dist/page/internal/layout-filter.js.map +1 -1
  181. package/dist/page/internal/metadata-helpers.d.ts +7 -0
  182. package/dist/page/internal/metadata-helpers.js +13 -2
  183. package/dist/page/internal/metadata-helpers.js.map +1 -1
  184. package/dist/page/internal/page-record.d.ts +34 -0
  185. package/dist/page/internal/page-record.js +87 -0
  186. package/dist/page/internal/page-record.js.map +1 -0
  187. package/dist/page/internal/page-registration.d.ts +2 -0
  188. package/dist/page/internal/page-registration.js.map +1 -1
  189. package/dist/page/layout-filter.js +31 -10
  190. package/dist/page/layout-filter.js.map +1 -1
  191. package/dist/page/metadata.js +36 -26
  192. package/dist/page/metadata.js.map +1 -1
  193. package/dist/page/types.d.ts +39 -4
  194. package/dist/page/types.js.map +1 -1
  195. package/dist/page.d.ts +1 -1
  196. package/dist/page.js +2 -1
  197. package/dist/page.js.map +1 -1
  198. package/dist/permissions-resolver.d.ts +26 -3
  199. package/dist/permissions-resolver.js +2 -1
  200. package/dist/permissions-resolver.js.map +1 -1
  201. package/dist/permissions.d.ts +7 -3
  202. package/dist/permissions.js +8 -6
  203. package/dist/permissions.js.map +1 -1
  204. package/dist/realtime.d.ts +24 -1
  205. package/dist/realtime.js +7 -1
  206. package/dist/realtime.js.map +1 -1
  207. package/dist/request-tenant.d.ts +17 -13
  208. package/dist/request-tenant.js +54 -42
  209. package/dist/request-tenant.js.map +1 -1
  210. package/dist/tenant-members.d.ts +26 -0
  211. package/dist/tenant-members.js +29 -0
  212. package/dist/tenant-members.js.map +1 -0
  213. package/dist/tenant-ownership.d.ts +4 -1
  214. package/dist/tenant-ownership.js +12 -17
  215. package/dist/tenant-ownership.js.map +1 -1
  216. package/dist/tenant-provisioning.d.ts +15 -0
  217. package/dist/tenant-provisioning.js +27 -0
  218. package/dist/tenant-provisioning.js.map +1 -0
  219. package/dist/tenant-scoped-model.d.ts +2 -1
  220. package/dist/tenant-scoped-model.js +4 -3
  221. package/dist/tenant-scoped-model.js.map +1 -1
  222. package/docs/1.core/10.layout-banners.md +57 -2
  223. package/docs/1.core/2.pages-and-modules.md +20 -11
  224. package/docs/1.core/4.permissions.md +34 -1
  225. package/docs/1.core/5.tenancy.md +37 -6
  226. package/docs/1.core/7.hooks.md +24 -4
  227. package/docs/1.core/8.realtime.md +24 -3
  228. package/docs/2.auth/2.authentication.md +3 -3
  229. package/docs/3.base/1.introduction.md +1 -0
  230. package/docs/3.base/10.display-blocks.md +20 -2
  231. package/docs/3.base/2.types.md +7 -5
  232. package/docs/3.base/3.forms.md +28 -8
  233. package/docs/3.base/4.tables.md +27 -9
  234. package/docs/3.base/5.charts-and-widgets.md +8 -4
  235. package/docs/3.base/6.layout.md +47 -1
  236. package/docs/3.base/8.data-types.md +16 -9
  237. package/docs/4.notifications/3.sending.md +4 -3
  238. package/docs/5.html-render/1.introduction.md +5 -7
  239. package/package.json +7 -11
@@ -89,12 +89,12 @@ export class NotesPage extends PageController(
89
89
  | `redirectOnSuccess` | `string` | Path to navigate to after a successful submit. Accepts `{{params.X}}` and `{{query.X}}` tokens like `submitUrl`, plus `{{response.X}}` which resolves against the JSON body of the submit response. |
90
90
  | `submitDefaults` | `Record<string, unknown>` | Values merged into the submit payload even when the field is absent from the form. String values accept `{{params.X}}` and `{{query.X}}` tokens resolved against the form's route at submit time; entries whose tokens do not resolve are dropped. |
91
91
  | `saveMode` | `FormSaveMode` | How the form offers to save. `"bar"` (default): a sticky bar shown while there are unsaved changes, naming them, with Discard and Save; on a page it floats over the form, taking no space while hidden. `"footer"`: buttons in the form's footer. `"none"`: no buttons. `"instant"`: each change saves on its own, the changed field alone sent to `submitUrl` (so the endpoint must accept a partial body), a pick at once and a text once typing pauses; a refused save is put back to the last confirmed value, with a Retry; an `"action"` form ignores it. In a drawer or modal, `"bar"` and `"footer"` both use the container's footer. |
92
- | `kind` | `FormKind` | What the form edits. `"record"` (default): keeps its values once saved, and offers Cancel while clean only when it has somewhere to go back to (the drawer or modal it sits in, or `backTo` on a page). `"action"`: a send, invite or run form; its `submitLabel` always shows in its footer, ready, so a pre-filled form sends its defaults as they are (a form with no field to fill in included); Reset joins it once a value changes (in a drawer or a modal, Cancel before that), and it empties after a successful submit (each field back to its `defaultValue`, or what `fetchUrl` loaded). |
92
+ | `kind` | `FormKind` | What the form edits. `"record"` (default): keeps its values once saved, and offers Cancel while clean only when it has somewhere to go back to (the drawer or modal it sits in, or `backTo` on a page). `"create"`: a form making a new record (a table view's add form and its duplicate, a relation's add form, set by `TableView` and `RelationType`); its submit, "Create" by default, shows from the start and it lists no changes. Declared, never inferred from `submitUrlMethod`. `"action"`: a send, invite or run form; its `submitLabel` always shows in its footer, ready, so a pre-filled form sends its defaults as they are (a form with no field to fill in included); Reset joins it once a value changes (in a drawer or a modal, Cancel before that), and it empties after a successful submit (each field back to its `defaultValue`, or what `fetchUrl` loaded). |
93
93
  | `backTo` | `string` | Where Cancel leads a `"record"` form on a page; without it the form offers no Cancel there. `TableView` sets it on its form pages, to the list. |
94
94
  | `labelKey` | `string` | Field of the loaded record that names it: on a page, the breadcrumb ends with its value once the form has loaded. `TableView` sets it on its edit and details pages from its own `labelKey`. Also in the page editor's Form schema, as an advanced option. |
95
95
  | `requiresRecord` | `boolean` | The form is about the record `fetchUrl` loads: while a route value that URL names is missing (a page reached without its `?id=`), it shows "No entry selected" instead of its fields, and cannot be saved. `ResourceForm` sets it in `edit` and `view`. |
96
96
 
97
- A form on a page that sets neither `saveMode` nor `kind` saves with the save bar, as a record: declare `kind: "action"` on a form that sends something new each time. `FORM_SAVE_MODES` and `FORM_KINDS` list the accepted values.
97
+ A form on a page that sets neither `saveMode` nor `kind` saves with the save bar, as a record: declare `kind: "create"` on a form that makes a new record and `kind: "action"` on a form that sends something new each time. `FORM_SAVE_MODES` and `FORM_KINDS` list the accepted values.
98
98
 
99
99
  An `"action"` form resets after a successful submit, a `"record"` keeps its values; `labelKey` names the field labelling the record.
100
100
 
@@ -112,9 +112,16 @@ A `FormField` describes one input.
112
112
  | `description` | `string` | Help text. |
113
113
  | `inputComponent` | `ComponentInfoSerialized` | Overrides the input component the data type would provide. |
114
114
  | `disabled` | `boolean` | Render the input non-editable. |
115
+ | `showEmptyOnCreate` | `boolean` | On a `"create"` form, keep this read-only field while it holds nothing; left out, an empty read-only field is hidden there. |
115
116
  | `required` | `boolean` | Whether a value must be supplied. Drives the schema, see below. |
116
117
  | `defaultValue` | `DefaultValue` | Initial value. |
118
+ | `offersDefault` | `boolean` | `defaultValue` is the module's default for a setting: while the field holds something else, it shows "Module default: X" with "Use default", which puts it back. Off unless set, so a record form never presents a column's initial value as a default. |
117
119
  | `localized` | `boolean` | The value is a per-locale record rather than a single value. |
120
+ | `fallbackLocale` | `string` | A localized field's fallback language: a value in any language requires one in this one. Set by a table view from `@Localized({ fallbackLocale })`. |
121
+ | `visibleWhen` | `FieldCondition` | `{ field, in }`: shown only while the field `field` holds one of the values `in` lists (`null` for an empty one), from the moment the form opens; hidden, it is neither required nor sent. |
122
+ | `disabledWhen` | `FieldCondition` | Same shape: read-only while the condition holds; its value is not sent. |
123
+
124
+ A module's own form applies `visibleWhen` and `disabledWhen` in the browser; its submit handler enforces what it needs. A table view's columns carry them to its forms and its data routes enforce them as well (see [Tables](/docs/interfaces/base/tables)).
118
125
 
119
126
  `FormFieldSerialized` replaces `type` with the data type's registered id string and `inputComponent` with a mandatory `component: ComponentInfoSerialized` — the resolved input component, taken from `inputComponent` when set and from `type.inputComponent()` otherwise.
120
127
 
@@ -211,13 +218,13 @@ function buildFormSchema(
211
218
  ```typescript
212
219
  function adaptFieldValidationSchema(
213
220
  baseSchema: z.ZodTypeAny,
214
- options: { localized?: boolean; required?: boolean },
221
+ options: FieldValidationOptions, // { localized?, required?, fallbackLocale? }
215
222
  ): z.ZodTypeAny;
216
223
  ```
217
224
 
218
225
  `adaptFieldValidationSchema` turns the schema a data type returns from `getValidation()` into the schema for one particular field:
219
226
 
220
- - With `localized: true` the schema is wrapped in `z.record(z.string(), schema)`, so the value becomes a map of locale to value.
227
+ - With `localized: true` the schema is wrapped in `z.record(z.string(), schema)`, so the value becomes a map of locale to value. With a `fallbackLocale` as well, a map filling any language must fill that one, else it fails with `$dms.field_errors.required` on `[fallbackLocale]`.
221
228
  - When `required` is falsy the schema becomes `.nullable().optional()`.
222
229
  - When `required` is true and the schema is a `ZodString` or a `ZodArray`, a `.min(1)` is added so an empty string or empty array does not satisfy the requirement.
223
230
 
@@ -275,6 +282,8 @@ const priority = FormComponents.InputSelect({
275
282
  | `File(options?)` | `FileOptions` |
276
283
  | `Image(options?)` | `ImageOptions` |
277
284
 
285
+ `InputWidth` is `"compact" | "full"`: how wide a single-line text, number or percentage input sits in its row. `"compact"` sizes it for a short value (11rem) from the `sm` breakpoint up, while a phone keeps the full row; `"full"` fills the row. Left out, the input is compact when its bounds keep the value short -- `min` / `max` within 6 digits, `maxLength` of 16 or less -- and fills the row otherwise.
286
+
278
287
  The option interfaces live in the same namespace. The ones with more than a placeholder:
279
288
 
280
289
  | Interface | Fields |
@@ -282,9 +291,9 @@ The option interfaces live in the same namespace. The ones with more than a plac
282
291
  | `SelectOption` | `label: string`, `value: string \| number`, `disabled?`, `icon?`, `iconColor?`, `textColor?` |
283
292
  | `SelectOptions` | `items: SelectOption[]`, `placeholder?`, `multiple?`, `deselectable?` |
284
293
  | `RadioGroupOptions` | `items: SelectOption[]`, `orientation?: AxeOrientation` |
285
- | `InputTextOptions` | `placeholder?`, `maxLength?`, `minLength?`, `showCount?` |
294
+ | `InputTextOptions` | `placeholder?`, `maxLength?`, `minLength?`, `showCount?`, `width?: InputWidth` |
286
295
  | `TextareaOptions` | `placeholder?`, `rows?`, `maxLength?`, `showCount?` |
287
- | `InputNumberOptions` | `min?`, `max?`, `step?`, `placeholder?`, `suffix?`, `clamp?` (`InputNumber` only: `false` keeps a value outside `min` / `max` as typed and reports it under the field instead of clamping it on blur) |
296
+ | `InputNumberOptions` | `min?`, `max?`, `step?`, `placeholder?`, `suffix?`, `clamp?` (`InputNumber` only: `false` keeps a value outside `min` / `max` as typed and reports it under the field instead of clamping it on blur), `width?: InputWidth` |
288
297
  | `InputTimeOptions` | `min?`, `max?`, `placeholder?` |
289
298
  | `InputPhoneOptions` | `placeholder?`, `requiredPrefix?` |
290
299
  | `InputPasswordOptions` | `placeholder?`, `minLength?`, `confirmPassword?`, `confirmPlaceholder?` |
@@ -316,14 +325,16 @@ The option interfaces live in the same namespace. The ones with more than a plac
316
325
  | `FormFunctions.SET_FIELD_DISABLED` | `"DmsComponent.Form.SetFieldDisabled"` |
317
326
  | `FormFunctions.SET_FIELD_HIDDEN` | `"DmsComponent.Form.SetFieldHidden"` |
318
327
  | `FormFunctions.SET_FIELD_REQUIRED` | `"DmsComponent.Form.SetFieldRequired"` |
328
+ | `FormFunctions.SET_FIELD_VALUE` | `"DmsComponent.Form.SetFieldValue"` |
319
329
 
320
- Both functions register their parameter shape in `WatchFunctionParamMap`, so `.watch()` type-checks the `params` object:
330
+ Each function registers their parameter shape in `WatchFunctionParamMap`, so `.watch()` type-checks the `params` object:
321
331
 
322
332
  | Function | Params |
323
333
  | --- | --- |
324
334
  | `SET_FIELD_DISABLED` | `{ targetField: string; setDisabled: boolean }` |
325
335
  | `SET_FIELD_HIDDEN` | `{ targetField: string; setHidden: boolean }` |
326
336
  | `SET_FIELD_REQUIRED` | `{ targetField: string; setRequired: boolean }` |
337
+ | `SET_FIELD_VALUE` | `SetFieldValueParams`: `{ targetField: string; value?: unknown; valueMap?: Record<string, unknown> }` |
327
338
 
328
339
  ```typescript
329
340
  import { Form, FormEvents, FormFunctions } from "@antelopejs/interface-dms/base/form";
@@ -340,7 +351,16 @@ const form = Form({
340
351
  });
341
352
  ```
342
353
 
343
- `FIELD_CHANGE` carries `{ fieldId, value }` and fires once per field with the values the form starts from (its defaults, or the record it loaded), then on each change: the watch above, which names no condition, hides `vatNumber` as soon as the form opens. Add a `value` condition to `onParam` for a watch that applies only when the field holds that value, whether it opened with it or the user picked it.
354
+ `FIELD_CHANGE` carries `{ fieldId, value, initial }` and fires once per field with the values the form starts from (its defaults, or the record it loaded), then on each change: the watch above, which names no condition, hides `vatNumber` as soon as the form opens. Add a `value` condition to `onParam` for a watch that applies only when the field holds that value, whether it opened with it or the user picked it. `initial` is `true` while the field still holds the value it started from and never held another.
355
+
356
+ `SET_FIELD_VALUE` writes a field from another one's change: `value` writes a fixed value, `valueMap` the entry named by the value of the field the event names, and a value the map has no entry for writes nothing. It skips the `initial` events, so a record opens with what it stored and a field nobody edited is never submitted as changed; a field it writes on a change counts as changed, like one typed in. A field it writes fires `FIELD_CHANGE` in turn, so two watches writing each other loop.
357
+
358
+ ```typescript
359
+ form.watch(FormEvents.FIELD_CHANGE, FormFunctions.SET_FIELD_VALUE, {
360
+ params: { targetField: "probability", valueMap: { won: 100, lost: 0 } },
361
+ onParam: { key: "fieldId", value: "stage" },
362
+ });
363
+ ```
344
364
 
345
365
  `.watch()` records the watch on the component itself; `.watchOn(childId, …)` records it on a child declared with `.child()`. Both come from `ComponentBuilder` in `@antelopejs/interface-dms/component`.
346
366
 
@@ -102,7 +102,7 @@ export class roleDataAPI extends DataController(
102
102
  | --- | --- |
103
103
  | `Get` | Read one row. |
104
104
  | `List` | Paginated list with search, filters and sorting. |
105
- | `Select` | List with the `select` pluck mode, used by relation pickers. |
105
+ | `Select` | List with the `select` pluck mode, used by relation pickers. `?ids=a&ids=b` (with `idKey` when the value is another field) answers those rows alone, as `List` does: a picker and a filter chip name the ids they hold this way. |
106
106
  | `Count` | Row count for the current query. |
107
107
  | `CountBatch` | One count per query, in a single `POST /count/batch` (tab, view and group counters). |
108
108
  | `Summary` | `GET /summary?ids=…`: the footer summaries a table view declared, over every row the request's filters list. |
@@ -137,9 +137,22 @@ These are not the plain data-api defaults. Every route asserts tenant access and
137
137
  | `group` | `string` | Identifier of the `ColumnGroup` this column belongs to in the generated forms. |
138
138
  | `cellWrap` | `boolean` | Wrap the grid cell without a line limit. |
139
139
  | `size` | `number` | Default width of the grid column in px (150 when omitted). |
140
+ | `visibleWhen` | `FieldCondition` | `{ field, in }`: the form field shows only while the column `field` holds one of the values `in` lists (`null` for an empty one). The data routes hold to it too: hidden, the field is not mandatory, the value sent for it is ignored, and an edit clears the value it stored. |
141
+ | `disabledWhen` | `FieldCondition` | Same shape: the form field is read-only while the condition holds, and the data routes ignore the value sent for it. |
140
142
  | `display` | `ColumnDisplay` | A display class instance (`new DefaultDisplays.IdentityDisplay({ … })`, or a module's own `@RegisterDisplay("<module>:<id>")` class) the grid cells (and expanded-row fields) render with, its options typed by the class. `type` keeps driving forms, filters, validation and exports; the formatter also receives the row. Serialized as `{ type, options? }`. |
141
143
 
142
- A read-only field whose `readonlyBehavior` resolves to `hidden` for a mode is dropped from that mode's form; a field whose data type has no input component is dropped from every form.
144
+ A field is read-only in a form when its `@Access` for the action that form runs is `AccessMode.ReadOnly`: the per-action override (`@Access(AccessMode.ReadWrite, { new: AccessMode.ReadOnly })`, keyed `new`, `edit` and `get` for the view form) where it declares one, else its default access — the access the data API applies to the value it writes. Such a field is disabled in that form, or dropped from it when its `readonlyBehavior` resolves to `hidden` for the mode; a field whose data type has no input component is dropped from every form.
145
+
146
+ `visibleWhen` and `disabledWhen` make a field depend on the value of another column, with no watch: the generated forms apply them from the values they open with, and the `new` and `edit` routes (`TableViewRoutes.New`, `Edit`, `EditWith`) apply them to what they write, read on the row as it will be stored — an edit's stored row under its body. A hidden or disabled field leaves the body before the guard runs, and an edit clears the value a hidden field stored (a disabled one keeps its own); `@Mandatory` binds a field with `visibleWhen` only while it shows, and there an edit must leave it filled (a deal moved to `lost` without its reason is refused with `400 { code: "missing_mandatory_fields", fields: ["lostReason"] }`, as the data API refuses any mandatory field). A condition naming an unknown column, the column itself or no value throws at registration.
147
+
148
+ ```ts
149
+ @Mandatory("new", "edit")
150
+ @Column({ name: "Loss reason", type: reasonType, visibleWhen: { field: "stage", in: ["lost"] } })
151
+ declare lostReason: string;
152
+
153
+ @Column({ name: "Probability", type: percentType, disabledWhen: { field: "stage", in: ["won", "lost"] } })
154
+ declare probability: number;
155
+ ```
143
156
 
144
157
  ### Column Groups
145
158
 
@@ -230,7 +243,7 @@ Typing `TableViewOptions` by `InstanceType<T>` is what makes row-action rules fi
230
243
  | `layout` | `TableViewLayout` | `"full"` (default): the dashboard grid; `"compact"`: the list of a settings page (tabs, open search, quick filters, the footer's page size picker; no caption, refresh, ⋯ menu nor column menus). Either way, search shows only with `@Searchable` fields, filters only with a `filterable` column, the caption only when `caption` is set. |
231
244
  | `searchPlaceholder` | `string` | Placeholder of the search field, also its accessible name. |
232
245
  | `searchShortcut` | `string` | Key opening and focusing the search, in Nuxt UI `defineShortcuts` notation (`"/"`, `"shift_f"`): `"shift_f"` (⇧F) by default. It does not fire while the user types in a field. |
233
- | `quickFilters` | `TableViewQuickFilter[]` | `{ field, label?, icon?, allLabel?, mode? }` dropdown filters over a filterable column's values, `mode` a `TableViewQuickFilterMode`. The picked value is the column's own filter: kept with the table state, counted, cleared by "Clear filters"; in the `compact` layout the button reads "Role: Admin" with a clear button. Unknown or non-filterable columns throw. |
246
+ | `quickFilters` | `TableViewQuickFilter[]` | `{ field, label?, icon?, allLabel?, mode? }` dropdown filters over a filterable column's values, `mode` a `TableViewQuickFilterMode`. The picked value is the column's own filter: kept with the table state, counted, cleared by "Clear filters"; in the `compact` layout the button reads "Role: Admin" with a clear button. Unknown or non-filterable columns throw. The column needs values to list (a select with `items`, a boolean, a status, a relation, or an input declaring `items` or a `searchUrl`): on any other — a free-text string, say — the toolbar leaves it out and registration logs a warning. |
234
247
  | `pageSize` | `number` | Rows per page while the user picked none (default 10), 1 to 50: registration throws above. A size other than 10, 25 and 50 joins them in the table's page size picker. |
235
248
  | `pagination` | `TableViewPaginationMode` | `"pages"` (default): a pager; `"loadMore"`: a "Load more" button; `"infinite"`: the next page loads as the footer scrolls into view. The last two ask for every page loaded so far, from the first row, so a refresh re-reads them. |
236
249
  | `footer` | `TableViewFooterOptions` | `{ countLabel?, hint?, summary?, legend? }`: the footer's row count (`$` key pluralized on `{ count }`) and hint; `summary` figures (`TableViewFooterSummary`: `label`, `field?`, `op: "sum" \| "count"`, `where?` row rule, `format?` `TableViewSummaryFormat`) computed by the `Summary` route over every listed row; `legend`, a `SelectType` column. Serialized as `TableViewFooterSerialized`: each summary gets an `id`, its `where` stays server-side (`TableViewMeta.footerSummary(id)`). A sum without `field`, an unknown column or a legend that is no select column throws. |
@@ -241,8 +254,10 @@ Typing `TableViewOptions` by `InstanceType<T>` is what makes row-action rules fi
241
254
  | `rowActions` | `TableViewRowActionOptions<T>` | Built-in and custom row actions. |
242
255
  | `columns` | `TableViewColumnsOptions` | The controller's columns as this table view shows them: `hidden` lists the ones it opens hidden, as `isVisible: false` does on the `@Column`, for this table view only (they stay in the column menu, filterable and in the forms). A name the controller has no column for throws at registration. |
243
256
  | `customButtons` | `CustomButton[]` | Toolbar buttons; see [Shared Types](/docs/interfaces/base/types). |
257
+ | `forms` | `Partial<Record<"new" \| "edit" \| "view", TableViewFormSetup>>` | A callback per generated form, `(form: FormBuilder) => void`, receiving its builder before the table view embeds it, for input comfort only: what it declares (`.watch()`, a value filled in from another, a field one table's forms leave out) holds in a drawer, a modal or a form page alike. No data rule here, the routes never see it: a field shown, required or locked by another's value is a column's `visibleWhen` / `disabledWhen`. |
244
258
  | `formContainer` | `FormContainer` | Where the `new` / `edit` / `view` forms open. |
245
259
  | `archiveMode` | `boolean` | Replace delete with archive/restore. Requires `@ArchiveField`. |
260
+ | `archivedCountLabel` | `string` | How the footer counts the archived rows: an i18n key with `$` receiving `{ count }` and pluralized on it, or a text with `{count}`. Defaults to "Archived: {count}", which agrees with no noun. |
246
261
  | `defaultFilters` | `Array<{ accessorKey: string; value?: string; mode: string }>` | Filters applied when the table opens. |
247
262
  | `defaultSort` | `{ field: string; desc?: boolean }` | Sort applied when the viewer has no stored preference, and the one a cleared sort goes back to. |
248
263
  | `pinnedRows` | `RowActionRule<T>` | Rows listed first whatever the sort, in the sort's order among themselves: within the list the route answers (a source without `paginate` answers every row, so its pinned rows open the first page; a paging route's open their page). The `grouped` display pins nothing; registration throws beside `reorder`. |
@@ -252,14 +267,15 @@ Typing `TableViewOptions` by `InstanceType<T>` is what makes row-action rules fi
252
267
  | `tabs` | `TableViewTab[]` | Tabs above the table, each applying a hidden filter on one column (`filter` optional). A tab may link to another page instead (`to`: page controller or path, `countFrom`: data controller or location for its counter); a tab with id `"all"` replaces the implicit one; `permission` (an action of the table) or `permissionId` (an absolute id) gates it; `navBadge` (`true`, or `{ tone }`) publishes its counter as the page's nav badge. Serialized as `TableViewTabSerialized` (`to` resolved per request, `toPage`). |
253
268
  | `realtime` | `boolean` | Set to `false` to disable realtime broadcast and presence for this resource. Realtime is on by default. |
254
269
  | `realtimeTopic` | `string \| string[]` | Topics the backend publishes on when rows change outside the table's routes: the table reads its rows again on each event. Registered against the page from `onCreated`; also offered by `TableView.fromSource`. |
255
- | `liveHold` | `boolean \| TableViewLiveHoldOptions` | On a page declared with `live`, keeps the rows while the page is paused and shows a "N new · Show" strip; Show reads them once without leaving the pause. `{ whileRowOpen?, whileScrolled?, whileSearching?, whileSorted? }` (all default `false`) also holds the table alone, the page staying live, while a row is open in the side panel, its first row is scrolled out of view, the user's search narrows the rows or they are sorted otherwise than by `defaultSort`; the strip then names the reason ("Table on hold · search active"). `missedLabel` words the strip's count, given `{count}` ("12 new requests"); while the page is paused the strip adds the page's `setMissedDetail` breakdown. A `TableView.fromSource` route receives `liveHeldAt` (ISO 8601) on every request made while held. Also offered by `TableView.fromSource`. |
270
+ | `liveHold` | `boolean \| TableViewLiveHoldOptions` | On a page declared with `live`, keeps the rows while the page is paused, and in Direct while they are on screen or scrolled past (a new row at the top would push them down), and shows a "N new · Show" pill over the rows, which moves nothing, counting the rows its tab, search and filters match now beyond those it holds (asked of its count route), not the events; Show reads them once without leaving the pause. `{ whileRowOpen?, whileScrolled?, whileSearching?, whileSorted? }` (all default `false`) also holds the table alone, the page staying live, while a row is open in the side panel, its first row is scrolled out of view, the user's search narrows the rows or they are sorted otherwise than by `defaultSort`; the strip then names the reason ("Table on hold · search active"). `missedLabel` words the strip's count, given `{count}` ("12 new requests"); while the page is paused the strip adds the page's `setMissedDetail` breakdown. A `TableView.fromSource` route receives `liveHeldAt` (ISO 8601) on every request made while held. Also offered by `TableView.fromSource`. |
256
271
  | `reorder` | `TableViewReorderOptions` | `{ field }`: rows ordered by hand with a drag handle, on the grid and on the `cards` display. `field` must be a `@Sortable()`, writable `NumberType` column, and the edit action on: registration throws otherwise. The rows are listed sorted on it, and a move saves only the rows whose value changed. |
257
272
  | `kanban` | `KanbanOptions` | Enable the kanban display. |
258
273
  | `grouped` | `GroupedOptions` | `{ groupByField, by?: "value" \| "day" \| "week", collapsible?, count? }`: offer the `grouped` display, the grid's columns under a header row per group. `groupByField` must be a `@Sortable()` column, a `DateType` one to group by day or week. |
259
274
  | `views` | `TableViewViewsOptions` | `{ items: TableViewView[], layout?: "tabs" \| "strip" \| "menu", defaultView?, userViews? }`. A `TableViewView` is a `TableViewViewState` (`filters`, `search`, `sort`, `columns: { visible?, hidden?, order? }`, `display`, `density`) with `id`, `label`, `icon`, `tone`, `dot`, `count`, `permission` (an action of the table by name, or an `Action`) and `permissionId` (an absolute permission id, which wins); a view the caller fails it for is left out when the layout is served. Filter values may hold `{{user.id}}`, `{{now}}` and `{{now±Nd}}`, resolved by the server. Serialized as `TableViewViewsSerialized` (no `permission` nor `permissionId`). |
260
- | `card` | `TableViewCardOptions` | The card the `kanban` and `cards` displays draw: `fields` it shows, or a `component` drawing it whole. |
275
+ | `card` | `TableViewCardOptions` | The card the `kanban` and `cards` displays draw: `fields` it shows, or a `component` drawing it whole; `minWidth` (a CSS length, `"230px"` by default) is the narrowest a card of the `cards` grid gets. |
261
276
  | `displays` | `TableViewDisplayOption[]` | The displays offered, in order: a built-in id (`"table"`, `"cards"`) without a component, or a module display named `<module>:<id>`; registration throws otherwise. Left out, the built-in `table` alone; listed, exactly these, so the grid is kept by listing `{ id: "table" }`. |
262
277
  | `defaultDisplay` | `string` | Display shown when the viewer has no stored preference. Defaults to `"table"`. |
278
+ | `mobileDisplay` | `string` | Display a phone (narrower than 768px) opens on when the viewer has no stored preference, e.g. `"cards"`. Registration throws when it is not among the displays offered. |
263
279
  | `guards` | `TableViewGuards<T>` | Server-side guards run before mutating actions. |
264
280
  | `bypassTenantAccessGate` | `boolean` | Skip the tenant access gate on every data route of this table view. |
265
281
 
@@ -269,7 +285,7 @@ Typing `TableViewOptions` by `InstanceType<T>` is what makes row-action rules fi
269
285
  TableView.fromSource(options: TableViewSourceOptions): ComponentBuilder<TableViewOptionsSerialized>;
270
286
  ```
271
287
 
272
- A read-only table view with no data controller (`tableViewFromSource`). `TableViewSourceOptions` takes `columns` (a record of `TableViewSourceColumn`, a `@Column`'s options plus `sortable`), `fetchUrl` (its `{{params.X}}` / `{{query.X}}` tokens are filled from the page URL, percent-encoded; the table and its tab counters list again when they change and request nothing while one has no value), `fetchRowUrl?` (a route answering one row by id, `{{row.id}}` its percent-encoded id, with the same tokens: a deep link to a row the table does not list opens through it; serialized on `TableViewSourceSerialized`), `rowIdKey?`, `periodScope?` (the id of a `PeriodSelector`: every request carries its `from`, `to`, `preset` and `comparison`, the table lists again when the period changes, and nothing is requested until the selector is on the page), `capabilities?` (`TableViewSourceCapabilities`: `search`, `sort`, `paginate`, `filter`), `queryParamFilters?` (URL query parameters sent to the route as hidden `filter_<field>` filters), custom row actions and buttons only, and the presentation options of a table view (`caption`, `layout`, `pagination`, `tabs`, `quickFilters`, `displays`, `card`, `emptyStates`, `footer` texts and legend…). The route answers `{ results, total }` and is sent the part of the list query its capabilities name, nothing else; the browser does the rest over the rows it answered, except that a paging route's rows are neither searched nor sorted by the browser. Registration runs the checks of `TableView()` that apply (page size, display ids, tab targets) and throws when tabs, quick filters or `queryParamFilters` lack `capabilities.filter`, or when a key they, `labelKey`, `card.fields` or `expandable` name is not a column, or when `expandable` sets `lazyLoad` (a source has no `get` route), or when `fetchRowUrl` lacks `{{row.id}}`. There is no export, realtime nor footer summary.
288
+ A read-only table view with no data controller (`tableViewFromSource`). `TableViewSourceOptions` takes `columns` (a record of `TableViewSourceColumn`, a `@Column`'s options plus `sortable`), `fetchUrl` (its `{{params.X}}` / `{{query.X}}` tokens are filled from the page URL, percent-encoded; the table and its tab counters list again when they change and request nothing while one has no value), `fetchRowUrl?` (a route answering one row by id, `{{row.id}}` its percent-encoded id, with the same tokens: a deep link to a row the table does not list opens through it; serialized on `TableViewSourceSerialized`), `rowIdKey?`, `periodScope?` (the id of a `PeriodSelector`: every request carries its `from`, `to`, `preset` and `comparison`, the table lists again when the period changes, and nothing is requested until the selector is on the page), `capabilities?` (`TableViewSourceCapabilities`: `search`, `sort`, `paginate`, `filter`), `queryParamFilters?` (URL query parameters sent to the route as hidden `filter_<field>` filters), custom row actions and buttons only, and the presentation options of a table view (`caption`, `layout`, `pagination`, `tabs`, `quickFilters`, `displays`, `card`, `emptyStates`, `footer` texts and legend…). The route answers `{ results, total }` and is sent the part of the list query its capabilities name, nothing else; the browser does the rest over the rows it answered, except that a paging route's rows are neither searched nor sorted by the browser. Registration runs the checks of `TableView()` that apply (page size, display ids, tab targets) and throws when tabs, quick filters or `queryParamFilters` lack `capabilities.filter`, or when a key they, `labelKey`, `card.fields` or `expandable` name is not a column, or when `expandable` sets `lazyLoad` (a source has no `get` route), or when `fetchRowUrl` lacks `{{row.id}}`; it warns about a quick filter on a column with no values to pick. There is no export, realtime nor footer summary.
273
289
 
274
290
  ### Reaching a Table Behind a Tenant Gate
275
291
 
@@ -358,7 +374,7 @@ A `new` or `edit` `ResourceForm` block over the controller submits to the same r
358
374
 
359
375
  `RowActionConfig` (built-in actions) also takes `label`, `icon` and `confirm` (an `ActionConfirm`: a `ConfirmDialog`, or `{ from }`, a URL answering one for the row — on delete, archive and restore, for a single row); `BulkRowActionConfig` (delete, archive, restore) adds `successMessage`. `CustomRowAction` also takes `confirm`, `permission` (`ButtonPermission`: an action of the table by name, or an `Action`), `permissionId` (an absolute permission id, which wins over `permission`), `color`, `variant`, `showLabel`, `bulk` (`true` or `{ allMatching: true }`: offered on the selection bar, see below) and `deepLink` (a drawer, modal or side panel it opens is kept in the URL as `?record=<id>` — by default for a side panel, which `false` opts out of —, `?<tableId>.record=<id>` on a page with several table views). An action the caller fails the permission for is stripped per request like a custom button; it is only hidden, so the route its target calls must check the permission itself.
360
376
 
361
- A bulk custom action sends the selected ids as a repeated `ids` query parameter, or `allMatching=true` with the table's filters, search and archive view after "Select all N matching". `resolveBulkRowIds(this, ctx, user)`, called from a route of the table's data controller, answers the ids the action covers: the selected ones, or every matching row read like the list reads them, refusing more than `MAX_BULK_MATCHING_ROWS` (413). Either way the caller needs the table's `list` permission (403 otherwise). `BULK_ALL_MATCHING_KEY` is the query flag. The `api` action target also takes `body` (the confirmation's field values merged in) and `copy` (response field copied to the clipboard). `CustomButton` takes `placement` (`"toolbar"`, the default, or `"header"`: drawn in the page header, which the server adds it to); `rowActions.add` takes it too (`AddRowActionConfig`).
377
+ A bulk custom action sends the selected ids as a repeated `ids` query parameter, or `allMatching=true` with the table's filters, search and archive view after "Select all N matching". `resolveBulkRowIds(this, ctx, user)`, called from a route of the table's data controller — or `resolveBulkRowIds({ model, tableView }, ctx, user)` (`BulkRowSource`: the model the calling route is given, and the table's `TableViewMeta`) from any other route, a page's included — answers the ids the action covers: the selected ones, or every matching row read like the list reads them, refusing more than `MAX_BULK_MATCHING_ROWS` (413). Either way the caller needs the table's `list` permission (403 otherwise). `BULK_ALL_MATCHING_KEY` is the query flag. The `api` action target also takes `body` (the confirmation's field values merged in) and `copy` (response field copied to the clipboard). `CustomButton` takes `placement` (`"toolbar"`, the default, or `"header"`: drawn in the page header, which the server adds it to); `rowActions.add` takes it too (`AddRowActionConfig`).
362
378
 
363
379
  When several table views share a data controller, each read action of its routes is guarded by the permission ids of every mounted table view, each write action by the writing table view's (`TableViewMeta.actionPermissionIds(action)`; any one suffices). `TableViewMeta.componentBuilders` lists them, `writingComponentBuilders` the ones that write.
364
380
 
@@ -442,7 +458,7 @@ static table = TableView(invoiceDataAPI, {
442
458
 
443
459
  `KanbanOptionsSerialized` is `KanbanOptions` plus the serialized `card` (`TableViewCardOptionsSerialized`).
444
460
 
445
- `card` (`TableViewCardOptions`: `{ fields?: string[]; component?: Component }`) is the card the `kanban` and `cards` displays draw for each row; it joins the options of both display entries. The component receives the same props in both: `row`, `rowId`, `columns`, `labelKey`, `actions`, `selected`, `select(value?)`, `open()`, on the kanban `groupValue`, and on the `cards` display of a table ordered by hand `reorder` (`{ enabled, dragging, handle }`, the card's move handle).
461
+ `card` (`TableViewCardOptions`: `{ fields?: string[]; component?: Component; minWidth?: string }`) is the card the `kanban` and `cards` displays draw for each row; it joins the options of both display entries. The component receives the same props in both: `row`, `rowId`, `columns`, `labelKey`, `actions`, `selected`, `select(value?)`, `open()`, on the kanban `groupValue`, and on the `cards` display of a table ordered by hand `reorder` (`{ enabled, dragging, handle }`, the card's move handle).
446
462
 
447
463
  ## Generated Forms
448
464
 
@@ -516,7 +532,9 @@ tableViewRowTopic("/api/orders"); // "tableview:row:/api/orders"
516
532
  tableViewPresenceTopic("/api/orders"); // "tableview:presence:/api/orders"
517
533
  ```
518
534
 
519
- The topics are registered with `RegisterPageTopic` from the table view's `onCreated`, so they belong to the module whose page it is, as described in [Realtime](/docs/interfaces/core/realtime). A module that publishes on a table's row topic itself — a background job updating rows, for instance — builds the name with `tableViewRowTopic` rather than spelling out the prefix.
535
+ The topics are registered with `RegisterPageTopic` from the table view's `onCreated`, so they belong to the module whose page it is, as described in [Realtime](/docs/interfaces/core/realtime). A module that publishes on a table's row topic itself — a background job updating rows, for instance — builds the name with `tableViewRowTopic` rather than spelling out the prefix, and passes the tenant of the rows as `tenantId`.
536
+
537
+ The topics are the same for every tenant, but what is published on them is not: the routes publish each change, and each presence, for the tenant the request was made in, so the sessions of the other tenants neither refetch nor see who has a row open. The DMS decides who else receives them, with no option on the table: a table whose rows live in the schema every tenant shares (`CORE_SCHEMA_NAME`) publishes to every tenant, and a session on a platform page (a module's page) receives the changes of every tenant, as such a page lists the data of all of them. `RealtimeMutationContext` and `RealtimePresenceContext` carry the tenant as `tenantId`, absent on a table of the shared schema.
520
538
 
521
539
  Two functions let other modules observe those mutations:
522
540
 
@@ -313,10 +313,11 @@ const card = ChartCard({
313
313
  | `currencyCode` | `string` | Currency for `valueFormat: "currency"`. |
314
314
  | `valuePrecision` | `ValuePrecision` | Fixed fraction digits or `"native"` Intl defaults; omitted keeps legacy formatting. Also applies to the previous value and nested chart's value axis and tooltip. |
315
315
  | `showDelta` | `boolean` | Show the change against the comparison period. |
316
+ | `invert` | `boolean` | Treat an increase as bad — for metrics where lower is better. |
316
317
  | `showLegend` | `boolean` | Show the primary / comparison legend. Defaults to `true`. |
317
318
  | `showSeriesLegend` | `boolean` | Show the nested chart's legend, naming each series. Defaults to `false`. |
318
319
  | `primaryLabel` | `string` | Label of the primary series. |
319
- | `comparisonLabel` | `string` | Label of the comparison series. |
320
+ | `comparisonLabel` | `string` | Label of the comparison series. Without it, a card with a `periodScope` names the selected comparison ("Previous year"). |
320
321
 
321
322
  ## KpiCard
322
323
 
@@ -345,17 +346,20 @@ const denied = KpiCard({
345
346
  | `icon` | `string` | Card icon. Defaults to `"i-ph-trend-up"` for the component metadata. |
346
347
  | `fetchUrl` | `string` | URL the card loads its value from. |
347
348
  | `fetchUrlMethod` | `HttpMethod` | Method for `fetchUrl`. |
348
- | `periodScope` | `string` | Id of the `PeriodSelector` to follow. |
349
+ | `periodScope` | `string` | Id of the `PeriodSelector` to follow. Its comparison names the change; "No comparison" hides the change, the comparison value and the label. |
350
+ | `realtimeTopic` | `string \| string[]` | Topics whose events make the card read `fetchUrl` again, through the page's `live` gate. The builder registers them on the page. |
349
351
  | `valueFormat` | `ValueFormat` | How the value is formatted. |
350
352
  | `currencyCode` | `string` | Currency for `valueFormat: "currency"`. |
351
353
  | `valuePrecision` | `ValuePrecision` | Fixed fraction digits or `"native"` Intl defaults; omitted keeps legacy formatting. |
352
354
  | `showDelta` | `boolean` | Show the change against the comparison period. |
355
+ | `showPreviousValue` | `boolean` | Show the route's `previousValue` next to the change ("vs €42,900"), formatted as the value. Defaults to `true`. |
353
356
  | `showSparkline` | `boolean` | Show the sparkline. |
354
357
  | `sparklineAccent` | `ChartColorValue \| "auto"` | Sparkline colour. |
355
358
  | `invert` | `boolean` | Treat an increase as bad — for metrics where lower is better. |
356
- | `compareLabel` | `string` | Label next to the delta. |
359
+ | `compareLabel` | `string` | Label next to the delta. Without it (and without `description`), a card with a `periodScope` names the selected comparison ("vs previous year"). Shown with the delta only: with no comparison, the `description` shows instead. |
357
360
  | `staticValue` | `number` | Inline value, as an alternative to `fetchUrl`. |
358
361
  | `staticDelta` | `number` | Inline delta, in percent (`12.5` reads +12.5 %). |
362
+ | `staticPreviousValue` | `number` | Inline value compared against, as a fetched `previousValue`. |
359
363
  | `staticSparkline` | `number[]` | Inline sparkline points. |
360
364
 
361
365
  ## TopListCard
@@ -450,7 +454,7 @@ const selector = PeriodSelector({
450
454
  | `size` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | Control size. |
451
455
  | `showRangeLabel` | `boolean` | Show the resolved date range as text. |
452
456
  | `rangeLabel` | `PeriodRangeLabel` | `"absolute"` (default): both bounds. `"relative"`: the start of a window reaching now, "since yesterday 11:52". |
453
- | `comparisonControl` | `PeriodComparisonControl` | `"menu"`, `"toggle"` (one inline button switching a comparison on and off) or `"hidden"` (nothing drawn, the comparison kept on). Defaults to `"toggle"` when `comparisons` offers a single comparison besides `"none"`, `"menu"` otherwise. |
457
+ | `comparisonControl` | `PeriodComparisonControl` | `"menu"`, `"toggle"` (one inline button switching a comparison on and off) or `"hidden"` (nothing drawn, the comparison kept on). Defaults to `"toggle"` when `comparisons` offers a single comparison besides `"none"`, `"menu"` otherwise. The menu's trigger names the compared window ("vs 16 – 17 Oct", the option's name in its tooltip and its list), so the range label beside it keeps the current window only. |
454
458
  | `rememberKey` | `string` | Selectors carrying the same key share the remembered last choice, on any page. |
455
459
  | `variant` | `PeriodSelectorVariant` | `"default"` or `"segmented"`. |
456
460
  | `maxDays` | `number` | The longest custom range, in days (both ends included), the user can pick in the calendar or the date fields. Presets and a range given by the URL are not limited. |
@@ -195,6 +195,7 @@ export class OverviewPage extends PageController(
195
195
  | `hideHeader` | `boolean` | Omit the page header. |
196
196
  | `fillHeight` | `boolean` | Let the page fill the height of the panel instead of growing with its content, for a tool that scrolls inside itself — see [Pages that fill the panel](#pages-that-fill-the-panel). Defaults to `false`. |
197
197
  | `headerActions` | `CustomButton[]` | Buttons right of the page title, in order — see [Header actions](#header-actions). |
198
+ | `record` | `PageRecordLoader` | Reads the record the page shows, with the layout, for the header buttons acting on it and every `availability` of the page — see [Buttons acting on the page's record](#buttons-acting-on-the-pages-record). |
198
199
 
199
200
  Dashboard pages are full-width unless they opt out, which is what a table, a grid of cards or a chart wants. Set `fullWidth: false` for a page that only holds a form, where a constrained column stays readable — or use `FormPageLayout`, which is exactly that:
200
201
 
@@ -275,7 +276,7 @@ A page scoped by its URL — `?workspace=web`, a record id in the path — decla
275
276
 
276
277
  A label's translation takes the page's route and query parameters as its own (a query parameter wins over a route parameter of the same name), so `"Download {locale}.json"` names the language of `?locale=fr` and follows it as the URL changes.
277
278
 
278
- An `availability` receives the page URL's query as `context.query` (one value per name; caller-supplied, so check it like any input). The header asks again whenever the query changes — only when one of its buttons declares an `availability` — and keeps the buttons it has until the answer lands. The keys the DMS writes for the view itself never cause a request: `record` (the row open in a side panel), `live` (the paused live mode) and a period selector's `<id>.period`, `<id>.compare`, `<id>.from`, `<id>.to`, `<id>.compareFrom` and `<id>.compareTo`. A query key may carry dots, so `{{query.api.period}}` reads a period selector's choice.
279
+ An `availability` receives the page URL's query as `context.query` (one value per name; caller-supplied, so check it like any input), its route parameters as `context.params` (`/projects/project/p1` on a page registered at `/projects/project/:id` is `{ id: "p1" }`), and the record the layout read as `context.record` ([Buttons acting on the page's record](#buttons-acting-on-the-pages-record)). The header asks again whenever the query or the route parameters change, and once a write refreshed the page — only when one of its buttons declares an `availability` or the layout reads a `record` — and keeps the buttons it has until the answer lands. The keys the DMS writes for the view itself never cause a request: `record` (the row open in a side panel), `live` (the paused live mode) and a period selector's `<id>.period`, `<id>.compare`, `<id>.from`, `<id>.to`, `<id>.compareFrom` and `<id>.compareTo`. A query key may carry dots, so `{{query.api.period}}` reads a period selector's choice.
279
280
 
280
281
  ```typescript
281
282
  DefaultLayout({
@@ -301,6 +302,35 @@ A table view of the page places its own buttons in the header with `placement: "
301
302
 
302
303
  Who sees a button is settled on the server when the layout is served: a button whose `permission` or `permissionId` the user lacks is left out of the layout, and a quick-action button only shows to the users the quick action is served to — that is, the users its page admits. A page drawn by a Vue page component can add its own buttons next to these with `usePageHeaderActions`; call it before the first `await` of the component's setup, so the server renders the buttons with the page and they never pop in. A page whose changes save as soon as they are made shows the shared **Saved instantly** pill there with `useInstantSaveHeader` ([Instant save or a save bar](/docs/components/lists-and-settings/section#instant-save-or-a-save-bar)).
303
304
 
305
+ #### Buttons acting on the page's record
306
+
307
+ A page about one record (a site, an invoice) reads it with its layout: `record` is a function the server calls each time it serves the layout, with the request's context — the page URL's `params` and `query`, the `user` and the `tenantId` — and answers the record, or `undefined` when there is none to show. A button's target names its fields as `{{record.X}}`, filled in by the server and percent-encoded: a maps link to the site's address, an API call on its own id. A button acting on the record — one whose target names `{{record.X}}`, or declaring `requiresRecord: true` (an "Edit" link that only needs `{{params.id}}`) — is left out of a page whose record was not found, or whose `record` threw (logged). Every `availability` of the page reads the record as `context.record`, so a button can be disabled on the record's state without reading it again:
308
+
309
+ ```typescript
310
+ DefaultLayout({
311
+ record: ({ params }) => GetModel(SiteModel).get(params?.id ?? ""),
312
+ headerActions: [
313
+ {
314
+ label: "$sites.directions",
315
+ icon: "i-ph-map-pin",
316
+ target: { type: "external", url: "https://www.google.com/maps/search/?api=1&query={{record.address}}", newTab: true },
317
+ },
318
+ { label: "$sites.edit", requiresRecord: true, target: { type: "page", url: "/sites/{{params.id}}/edit" } },
319
+ {
320
+ label: "$sites.close",
321
+ target: { type: "api", url: "/api/sites/close?id={{params.id}}" },
322
+ availability: ({ record }) => (record?.closed ? { reason: "$sites.already_closed" } : undefined),
323
+ },
324
+ ],
325
+ });
326
+ ```
327
+
328
+ The record travels with the layout, so the header is drawn with it, on the server too, and never waits for it. Like an `availability`, it follows the page URL: the header asks for its buttons again when the route parameters or the query change, and once a write refreshed the page (`refreshPageBlocks()`, which a header button calls after a change, and a form once it saved). Its fields only shape the buttons; the route behind each one still checks the record on its own.
329
+
330
+ #### A record deleted under the page
331
+
332
+ A page reading a `record` leaves it once it is deleted, in every tab and for every member on it, with no code of the module. On each event of one of the page's realtime topics (its table views' rows, a `realtimeTopic`, a topic bound with `RegisterPageTopic`), the page asks its layout again: a record the server found before and now finds none of (its `record` answers `undefined`) was deleted. A toast says so, and the page opens the list it is a detail of (`MenuOptions.parent`); without one, it stays, on the "not found" state of its blocks. A `record` that throws counts as unknown, never as deleted, and a page opened on a record that never existed shows its blocks' "not found" state without a toast. Publish on one of the page's topics when the record is deleted: a table view's delete does it on its own.
333
+
304
334
  #### Hidden or disabled
305
335
 
306
336
  An `availability` has three answers. `undefined` leaves the button as declared, `{ reason }` disables it with the reason in a tooltip — for something the user can change ("No seat left: upgrade the plan") — and `{ hidden: true }` leaves it out of the header, as a missing permission does — for a button that makes no sense in the scope the page shows. Like a disabled one, a hidden button follows the page URL: the header asks again when the query changes, so a button can disappear on one `?workspace=` and come back on another, and the server-rendered header already carries the answer for the URL it was rendered for.
@@ -320,6 +350,22 @@ DefaultLayout({
320
350
  });
321
351
  ```
322
352
 
353
+ A detail page reads its record from `context.params`, and hides the buttons that act on it when the record does not exist — the page then shows its "not found" state with no "Edit" or "Delete" beside the title:
354
+
355
+ ```typescript
356
+ DefaultLayout({
357
+ headerActions: [
358
+ {
359
+ id: "edit",
360
+ label: "$projects.edit",
361
+ target: { type: "page", url: "/projects/project/{{params.id}}/edit" },
362
+ availability: async ({ params }) =>
363
+ (await projectExists(params?.id)) ? undefined : { hidden: true },
364
+ },
365
+ ],
366
+ });
367
+ ```
368
+
323
369
  Hiding only shapes the UI: the route behind the button must still refuse the operation on its own.
324
370
 
325
371
  #### Ordering the header
@@ -73,6 +73,7 @@ abstract class DataType {
73
73
  filter(context, proxy, key, value, mode, row): ValueProxyOrValue<boolean>;
74
74
  filterComponents(): Record<string, ComponentInfoSerialized> & { default: ComponentInfoSerialized };
75
75
  inputComponent(): ComponentInfoSerialized;
76
+ describedInputComponent(): ComponentInfoSerialized;
76
77
  decorateField?(target: unknown, key: PropertyKey, descriptor?: PropertyDescriptor): void;
77
78
  }
78
79
  ```
@@ -85,6 +86,7 @@ abstract class DataType {
85
86
  | `defaultInputComponent()` | Abstract. Returns the input component the type renders. Implemented by every concrete type. |
86
87
  | `getValidation()` | Abstract. Returns the Zod schema for one value of this type, before the field-level adaptation described in [Forms](/docs/interfaces/base/forms). |
87
88
  | `inputComponent()` | The form input. Defaults to `defaultInputComponent()`. |
89
+ | `describedInputComponent()` | The input as it is described where nothing is entered through it: a table column's type (its cells, cards and quick filters read the options) and a read-only form field. Defaults to `inputComponent()`; a type leaves out what only an editable control uses, as a relation leaves out its add form. |
88
90
  | `filterComponents()` | The filter inputs, keyed by compare-mode id with a mandatory `default`. Defaults to `{ default: defaultInputComponent() }`. |
89
91
  | `filter()` | Applies a filter for one compare mode. The base implementation looks the mode up in the compare-mode registry and delegates to it; a type overrides it when a mode needs different semantics. |
90
92
  | `decorateField()` | Optional. Called by the `@Column` decorator so the type can add further decorators to the field — this is how the relation types install their join. |
@@ -207,7 +209,7 @@ function serializeType(instance?: DataType): DataTypeSerialized | undefined;
207
209
  | `compareModes` | `string[]` | Ids of the type's compare modes; unregistered ones are dropped. |
208
210
  | `defaultCompareMode` | `string` | Id of the default mode. |
209
211
  | `filterComponents` | `Record<string, FilterComponent>` | Filter inputs by mode id, from `filterComponents()`, with every `noInput` mode's entry replaced by the literal `"noInput"`. |
210
- | `inputComponent` | `ComponentInfoSerialized` | The form input, from `inputComponent()`. |
212
+ | `inputComponent` | `ComponentInfoSerialized` | The input the column's cells and quick filters read the options of, from `describedInputComponent()`. |
211
213
 
212
214
  `FilterComponent` is `ComponentInfoSerialized | "noInput"`. The value-level counterpart, `ComponentOrNoInput`, is described in [Shared Types](/docs/interfaces/base/types).
213
215
 
@@ -232,7 +234,7 @@ function serializeType(instance?: DataType): DataTypeSerialized | undefined;
232
234
  | `NotContains` | `not_contains` | value |
233
235
  | `ArrayContainsString` | `array_contains_string` | value |
234
236
 
235
- `IsEmpty` and `IsNotEmpty` are the two registered with `noInput`. The namespace also exports `escapeRegex(str: string): string`, the helper the substring modes use to make a viewer-supplied value safe inside a pattern.
237
+ `IsEmpty` and `IsNotEmpty` are the two registered with `noInput`. `Include`, `Exclude` and `ArrayContainsString` read a `null` or missing list as an empty one, so a row saved without a value never fails the query: it is left out by `include` and `array_contains_string`, and kept by `exclude`. The namespace also exports `escapeRegex(str: string): string`, the helper the substring modes use to make a viewer-supplied value safe inside a pattern.
236
238
 
237
239
  ## The Default Types
238
240
 
@@ -264,12 +266,13 @@ function serializeType(instance?: DataType): DataTypeSerialized | undefined;
264
266
 
265
267
  | Options interface | Fields |
266
268
  | --- | --- |
267
- | `StringTypeOptions` | `placeholder?`, `maxLength?`, `minLength?`, `showCount?`, `textarea?`, `rows?`, `fallback?` |
268
- | `NumberTypeOptions` | `min?`, `max?`, `step?`, `placeholder?`, `suffix?`, `fallback?` |
269
+ | `StringTypeOptions` | `placeholder?`, `maxLength?`, `minLength?`, `showCount?`, `textarea?`, `rows?`, `width?`, `fallback?` |
270
+ | `NumberTypeOptions` | `min?`, `max?`, `step?`, `placeholder?`, `suffix?`, `width?`, `fallback?` |
269
271
  | `DateTypeOptions` | `range?`, `multiple?`, `minDate?`, `maxDate?`, `fallback?` |
270
272
  | `BooleanTypeOptions` | `fallback?` |
271
273
  | `SelectTypeOptions` | `items: FormComponents.SelectOption[]`, `placeholder?`, `multiple?`, `deselectable?`, `display?`, `columns?` (cards per row of the `cards` display, 2 by default), `fallback?` |
272
- | `EmailTypeOptions`, `ColorTypeOptions`, `UrlTypeOptions`, `RichTextTypeOptions` | `placeholder?`, `fallback?` |
274
+ | `EmailTypeOptions`, `ColorTypeOptions`, `RichTextTypeOptions` | `placeholder?`, `fallback?` |
275
+ | `UrlTypeOptions` | `placeholder?`, `preview?` (`"avatar"` \| `"image"`: shown read-only, the URL is drawn as the image it points to; the form still edits it as text), `fallback?` |
273
276
  | `PasswordTypeOptions` | `placeholder?`, `minLength?`, `confirmPassword?`, `confirmPlaceholder?`, `fallback?` |
274
277
  | `PhoneTypeOptions` | `placeholder?`, `requiredPrefix?`, `fallback?` |
275
278
  | `StringTimeTypeOptions` | `placeholder?`, `min?`, `max?`, `fallback?` |
@@ -279,6 +282,7 @@ Notes on the ones whose behaviour is not obvious from the options:
279
282
  - `StringType` renders a textarea instead of a single-line input when `textarea` is set, and passes `rows` to it.
280
283
  - `StringType` with `showCount` shows the characters typed, out of `maxLength` when set ("4 / 60"), in its input or under its textarea.
281
284
  - `NumberType` hands `suffix` (an i18n key with `$`, or a literal) to its input, which writes it after the field; the value stays a number. `PriceType` inherits it; `PercentageType` keeps its own `%`.
285
+ - `NumberType` (and the types extending it) and a single-line `StringType` hand `width` (`FormComponents.InputWidth`) to their input: `"compact"` sizes it for a short value from `sm` up, `"full"` fills the row, and left out the input is compact when `min` / `max` fit in 6 digits or `maxLength` is 16 or less.
282
286
  - `DateType` overrides `filter` so `is`, `is_not` and `is_between` compare whole UTC days rather than instants, and adds a range picker as the filter component for `is_between`. Its schema accepts a `Date` or an ISO-shaped string and transforms it to a `Date`; with `multiple` the value becomes an array, and with `range` it becomes `{ start, end }`, `end` on or after `start` (a pair `[start, end]` is still read and validates to the same object).
283
287
  - `BooleanType` uses a two-option select as its filter component, so a viewer picks "checked"/"unchecked" rather than typing a value.
284
288
  - `SelectType` validates against the `value`s of its `items`, as an enum or an array of that enum when `multiple` is set.
@@ -315,13 +319,16 @@ declare customerId: string;
315
319
  | `placeholder` | `string` | Picker placeholder. |
316
320
  | `multiple` | `boolean` | Allow more than one related row. |
317
321
  | `deselectable` | `boolean` | Allow clearing the selection. |
318
- | `filterOnly` | `boolean` | Skip the join entirely: the raw scalar id stays in the list output and the funnel filter runs equality on it. |
322
+ | `filterOnly` | `boolean` | Skip the join entirely: the raw scalar id stays in the list output and the funnel filter runs equality on it. A table cell, a card or a kanban card of that column then shows the id: lists draw the joined row only, they never read the ids by `?ids=`. |
319
323
  | `addForm` | `{ displayName?: string; description?: string }` | Title and description of the drawer the picker's "add" entry opens ("New customer"). `$`-prefixed: i18n keys. Default to "New entry". |
324
+ | `filterBy` | `{ field, from }` | List only the target rows whose `field` equals the form's current value of the field `from`, e.g. `{ field: "customerId", from: "customerId" }` for a site among its customer's sites. `field` must be filterable on the target (`filterable: true` or `@Filter`); `decorateField` throws otherwise. |
320
325
  | `fallback` | `string` | Text used in place of an empty value. |
321
326
 
322
- When the target controller exposes a `new` endpoint and has form fields, the picker also carries an inline "add" form built from the target's `new` fields, plus the permission id of the target's `add` action.
327
+ With `filterBy`, the picker sends the table filter syntax to the `select` endpoint (`filter_<field>=is:<value>`), which honors only the filters the target controller declares. It searches again when `from` changes and clears a pick made for its previous value; the pair a record loaded with is kept, and while `from` is empty every row is offered. It narrows what is offered only: a rule that must hold on save belongs to the module's routes.
323
328
 
324
- `CascaderRelationType<T>` is the same idea for a self-referencing table: the target's `select` endpoint returns a flat list, and the widget rebuilds the tree client-side. Its `keyMapping` is required and adds `parent` — the field pointing at the parent row, which must itself carry `@Select()` — alongside the required `label` and `value`. It adds `maxDepth` and `leafOnly` to the relation options, and supports the same `filterOnly` behaviour. In a table cell it shows the path of the picked node ("Electronics / Audio"); a `multiple` one shows a badge per picked node, named by its own label with its path on hover, and folds those past three into `+N`.
329
+ When the target controller exposes a `new` endpoint and has form fields, the picker of an editable form field also carries an inline "add" form built from the target's `new` fields, plus the permission id of the target's `add` action. That form is the bulk of the component — the target's fields, its validation schema, its own relations' add forms — so a table column's type, its filter and a read-only field leave it out: they read the related rows through the `select` route, and a filter only picks among them.
330
+
331
+ `CascaderRelationType<T>` is the same idea for a self-referencing table: the target's `select` endpoint returns a flat list, and the widget rebuilds the tree client-side. Its cell and its field name the picked row by its path, read by `?ids=` for the picked rows and then each level of their ancestors. Its `keyMapping` is required and adds `parent` — the field pointing at the parent row, which must itself carry `@Select()` — alongside the required `label` and `value`. It adds `maxDepth` and `leafOnly` to the relation options, and supports the same `filterOnly` behaviour. In a table cell it shows the path of the picked node ("Electronics / Audio"); a `multiple` one shows a badge per picked node, named by its own label with its path on hover, and folds those past three into `+N`.
325
332
 
326
333
  ### Address
327
334
 
@@ -338,7 +345,7 @@ When the target controller exposes a `new` endpoint and has form fields, the pic
338
345
  | `addressLine2` | no | Supplementary delivery information. |
339
346
  | `countrySubdivision` | no | Region, state or province. |
340
347
 
341
- The type's options are `placeholder` (one string per address component), `autocomplete`, an optional `dataApiController` with a `keyMapping` of `label` / `value` for picking an existing address, and `fallback`. `AddressAutocomplete` configures the geocoder:
348
+ The type's options are `placeholder` (one string per address component), `autocomplete`, `defaultCountry` (the ISO 3166-1 alpha-2 country shown while the address has none and whose places the street suggestions list first, written with the first part typed: an address left empty stays empty), an optional `dataApiController` with a `keyMapping` of `label` / `value` for picking an existing address, and `fallback`. `AddressAutocomplete` configures the geocoder:
342
349
 
343
350
  | Option | Type | Description |
344
351
  | --- | --- | --- |
@@ -33,7 +33,7 @@ A `SendableNotification` is immutable and holds no recipient, so you can build o
33
33
  | `toUser` | `(userId: string, options?: SendOptions) => Promise<void>` | Send to one user. |
34
34
  | `toUsers` | `(userIds: string[], options?: SendOptions) => Promise<void>` | Send to a list of users. |
35
35
  | `toUsersIdempotently` | `(userIds: string[], idempotencyKey: string, options?: SendOptions) => Promise<void>` | Send a durable event once per recipient, including after dismissal. |
36
- | `toRoles` | `(roleIds: string[], options?: SendOptions) => Promise<void>` | Send to every user holding at least one of the roles. |
36
+ | `toRoles` | `(roleIds: string[], options?: RoleSendOptions) => Promise<void>` | Send to every user holding at least one of the roles, in `options.tenantId` when set. |
37
37
  | `broadcast` | `(options?: SendOptions) => Promise<void>` | Send to every user. |
38
38
 
39
39
  ## Building a Notification
@@ -146,10 +146,10 @@ await shipped.toUsers([ownerId, managerId], { readScope: "shared" });
146
146
  `toRoles(roleIds, options?)` resolves the users that hold any of the given roles and sends to them. An empty `roleIds` array sends nothing, and so does a set of roles nobody holds.
147
147
 
148
148
  ```typescript
149
- await shipped.toRoles([adminRoleId], { readScope: "shared" });
149
+ await shipped.toRoles([adminRoleId], { readScope: "shared", tenantId });
150
150
  ```
151
151
 
152
- Role ids are the DMS role identifiers described in the DMS tenancy and permissions guide; membership is resolved across tenants, so a user holding the role in any tenant is included.
152
+ Role ids are the DMS role identifiers described in the DMS tenancy and permissions guide, and they belong to a tenant. `tenantId` (`RoleSendOptions`) restricts the recipients to that tenant's members; without it, membership is resolved across tenants, so a user holding the role in any tenant is included.
153
153
 
154
154
  ## Broadcasting
155
155
 
@@ -168,6 +168,7 @@ Every dispatch method accepts an optional `SendOptions` object.
168
168
  | `readScope` | `ReadScope` (optional) | Read-state grouping for a multi-recipient send. Defaults to `"individual"` — see [Read Scope](/docs/interfaces/notifications/categories-and-subjects#read-scope). |
169
169
  | `idempotencyKey` | `string` (optional) | Stable event key, unique across producers: prefix it with your module ID. Reusing it for a recipient preserves the original notification and dismissal state. |
170
170
  | `dedupe` | `boolean` (optional) | `false` stores every unkeyed send, even one identical to a notification the recipient got less than 10 seconds earlier. Defaults to `true`; see below. |
171
+ | `tenantId` | `string` (optional, `toRoles` only: `RoleSendOptions`) | Only the members of this tenant holding the roles; every tenant when unset. |
171
172
 
172
173
  Pick `"shared"` for alerts that only need one person to act on them, and `"individual"` for anything each recipient has to see for themselves.
173
174
 
@@ -255,7 +255,7 @@ Types the `htmlRender` block of the DMS module configuration in `antelope.config
255
255
  | Option | Type | Description |
256
256
  | ------ | ---- | ----------- |
257
257
  | `renderEndpoint` | `string` | Absolute URL `GenerateHtml` posts to. Default `http://localhost:3001/api/html/render`. |
258
- | `serviceSecret` | `string` | Secret used to sign the service token that authenticates the render call. Default `"dev"` — set a strong value in production. |
258
+ | `serviceSecret` | `string` | Secret used to sign the service token that authenticates the render call. Default `"dev"`, which the DMS refuses in production. |
259
259
  | `serviceTokenLifetime` | `number` | Lifetime of that service token, in milliseconds. The frontend render server rejects a token valid for more than five minutes. Default `300000` (5 minutes). |
260
260
 
261
261
  The DMS merges the block you supply over those defaults, so a configuration only needs the keys it changes:
@@ -263,13 +263,12 @@ The DMS merges the block you supply over those defaults, so a configuration only
263
263
  ```typescript
264
264
  import { defineConfig } from "@antelopejs/interface-core/config";
265
265
 
266
- const renderSecret = process.env.DMS_HTML_RENDER_SECRET;
267
- if (!renderSecret) {
268
- throw new Error("DMS_HTML_RENDER_SECRET is not set");
269
- }
270
-
271
266
  export default defineConfig({
272
267
  name: "acme-admin",
268
+ // Read at start; a production start without it is refused by the DMS.
269
+ envOverrides: {
270
+ DMS_HTML_RENDER_SECRET: "modules.dms.config.htmlRender.serviceSecret",
271
+ },
273
272
  modules: {
274
273
  dms: {
275
274
  source: {
@@ -280,7 +279,6 @@ export default defineConfig({
280
279
  config: {
281
280
  htmlRender: {
282
281
  renderEndpoint: "https://admin.example.com/api/html/render",
283
- serviceSecret: renderSecret,
284
282
  },
285
283
  },
286
284
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-dms",
3
- "version": "0.7.4",
3
+ "version": "0.9.0",
4
4
  "description": "Public AntelopeJS interfaces of the DMS: pages, components, permissions, tenancy, auth, notifications and HTML rendering.",
5
5
  "keywords": [
6
6
  "antelopejs",
@@ -45,7 +45,6 @@
45
45
  "knip:all": "knip"
46
46
  },
47
47
  "dependencies": {
48
- "jsonwebtoken": "^9.0.3",
49
48
  "randomstring": "^1.3.1",
50
49
  "zod": "~3.24.2",
51
50
  "zod-to-json-schema": "~3.24.6"
@@ -53,23 +52,20 @@
53
52
  "peerDependencies": {
54
53
  "@antelopejs/interface-api": ">=0.0.15 <1.0.0",
55
54
  "@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
56
- "@antelopejs/interface-auth": ">=0.0.7 <1.0.0",
57
55
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
58
- "@antelopejs/interface-data-api": ">=0.2.2 <1.0.0",
59
- "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
60
- "@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0",
56
+ "@antelopejs/interface-data-api": ">=0.4.0 <1.0.0",
57
+ "@antelopejs/interface-database": ">=0.1.11 <1.0.0",
58
+ "@antelopejs/interface-database-decorators": ">=0.1.10 <1.0.0",
61
59
  "@antelopejs/interface-file-storage": ">=0.1.4 <1.0.0"
62
60
  },
63
61
  "devDependencies": {
64
62
  "@antelopejs/interface-api": ">=0.0.15 <1.0.0",
65
63
  "@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
66
- "@antelopejs/interface-auth": ">=0.0.7 <1.0.0",
67
64
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
68
- "@antelopejs/interface-data-api": ">=0.2.2 <1.0.0",
69
- "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
70
- "@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0",
65
+ "@antelopejs/interface-data-api": ">=0.4.0 <1.0.0",
66
+ "@antelopejs/interface-database": ">=0.1.11 <1.0.0",
67
+ "@antelopejs/interface-database-decorators": ">=0.1.10 <1.0.0",
71
68
  "@antelopejs/interface-file-storage": ">=0.1.4 <1.0.0",
72
- "@types/jsonwebtoken": "^9.0.10",
73
69
  "@types/node": "^24.0.0",
74
70
  "@types/randomstring": "^1.3.0",
75
71
  "knip": "^6.34.0",