@antelopejs/interface-dms 0.7.4 → 0.8.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.
- package/CHANGELOG.md +107 -0
- package/dist/auth/index.d.ts +3 -3
- package/dist/auth/index.js +11 -9
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/internal/jwt-auth-decorator.d.ts +20 -0
- package/dist/auth/internal/jwt-auth-decorator.js +51 -0
- package/dist/auth/internal/jwt-auth-decorator.js.map +1 -0
- package/dist/auth/internal/request-authenticators.d.ts +12 -0
- package/dist/auth/internal/request-authenticators.js +22 -0
- package/dist/auth/internal/request-authenticators.js.map +1 -1
- package/dist/base/activity-feed.d.ts +6 -6
- package/dist/base/block-registry/types.d.ts +16 -0
- package/dist/base/block-types.d.ts +1 -0
- package/dist/base/block-types.js +1 -0
- package/dist/base/block-types.js.map +1 -1
- package/dist/base/chart-card.d.ts +15 -0
- package/dist/base/chart-card.js +4 -0
- package/dist/base/chart-card.js.map +1 -1
- package/dist/base/chart-schemas.d.ts +26 -26
- package/dist/base/code-block.d.ts +3 -3
- package/dist/base/data-types/compare-types.js +19 -13
- package/dist/base/data-types/compare-types.js.map +1 -1
- package/dist/base/data-types/core.d.ts +11 -0
- package/dist/base/data-types/core.js +14 -1
- package/dist/base/data-types/core.js.map +1 -1
- package/dist/base/data-types/default-types.d.ts +112 -2
- package/dist/base/data-types/default-types.js +90 -19
- package/dist/base/data-types/default-types.js.map +1 -1
- package/dist/base/data-types/field-types.d.ts +10 -4
- package/dist/base/data-types/field-types.js +12 -2
- package/dist/base/data-types/field-types.js.map +1 -1
- package/dist/base/data-types/internal/compare-types.d.ts +3 -0
- package/dist/base/data-types/internal/compare-types.js +7 -0
- package/dist/base/data-types/internal/compare-types.js.map +1 -1
- package/dist/base/empty-state.d.ts +4 -4
- package/dist/base/form-block-schema.d.ts +59 -3
- package/dist/base/form-block-schema.js +12 -2
- package/dist/base/form-block-schema.js.map +1 -1
- package/dist/base/form-schema.d.ts +46 -1
- package/dist/base/form-schema.js.map +1 -1
- package/dist/base/form-types.d.ts +56 -13
- package/dist/base/form-types.js +12 -4
- package/dist/base/form-types.js.map +1 -1
- package/dist/base/guard.d.ts +6 -6
- package/dist/base/helpers/archive-mode-helpers.js +6 -2
- package/dist/base/helpers/archive-mode-helpers.js.map +1 -1
- package/dist/base/index.d.ts +1 -0
- package/dist/base/index.js +1 -0
- package/dist/base/index.js.map +1 -1
- package/dist/base/internal/form-schema.js +2 -0
- package/dist/base/internal/form-schema.js.map +1 -1
- package/dist/base/internal/layouts.d.ts +4 -0
- package/dist/base/key-value-list.d.ts +12 -9
- package/dist/base/key-value-list.js +1 -1
- package/dist/base/key-value-list.js.map +1 -1
- package/dist/base/kpi-card.d.ts +39 -5
- package/dist/base/kpi-card.js +18 -6
- package/dist/base/kpi-card.js.map +1 -1
- package/dist/base/layouts.d.ts +24 -0
- package/dist/base/layouts.js.map +1 -1
- package/dist/base/live-toggle.d.ts +11 -0
- package/dist/base/live-toggle.js +8 -1
- package/dist/base/live-toggle.js.map +1 -1
- package/dist/base/page-toolbar.d.ts +158 -0
- package/dist/base/page-toolbar.js +198 -0
- package/dist/base/page-toolbar.js.map +1 -0
- package/dist/base/period-selector.d.ts +9 -2
- package/dist/base/period-selector.js.map +1 -1
- package/dist/base/placeholder.d.ts +2 -2
- package/dist/base/section.d.ts +2 -2
- package/dist/base/stack.d.ts +2 -2
- package/dist/base/stat-group.d.ts +18 -6
- package/dist/base/stat-group.js +9 -0
- package/dist/base/stat-group.js.map +1 -1
- package/dist/base/table-view/column-display.d.ts +9 -5
- package/dist/base/table-view/column-display.js +1 -1
- package/dist/base/table-view/column-display.js.map +1 -1
- package/dist/base/table-view/data-functions.d.ts +26 -7
- package/dist/base/table-view/data-functions.js +10 -6
- package/dist/base/table-view/data-functions.js.map +1 -1
- package/dist/base/table-view/factory.js +24 -13
- package/dist/base/table-view/factory.js.map +1 -1
- package/dist/base/table-view/internal/auth.d.ts +2 -0
- package/dist/base/table-view/internal/auth.js +3 -1
- package/dist/base/table-view/internal/auth.js.map +1 -1
- package/dist/base/table-view/internal/factory-helpers.d.ts +5 -0
- package/dist/base/table-view/internal/factory-helpers.js +55 -11
- package/dist/base/table-view/internal/factory-helpers.js.map +1 -1
- package/dist/base/table-view/internal/guards.js +19 -2
- package/dist/base/table-view/internal/guards.js.map +1 -1
- package/dist/base/table-view/internal/realtime.js +17 -0
- package/dist/base/table-view/internal/realtime.js.map +1 -1
- package/dist/base/table-view/internal/resource-form.js +1 -0
- package/dist/base/table-view/internal/resource-form.js.map +1 -1
- package/dist/base/table-view/internal/row-actions-schema.d.ts +296 -0
- package/dist/base/table-view/internal/row-actions-schema.js +62 -0
- package/dist/base/table-view/internal/row-actions-schema.js.map +1 -0
- package/dist/base/table-view/internal/validation.d.ts +15 -3
- package/dist/base/table-view/internal/validation.js +70 -2
- package/dist/base/table-view/internal/validation.js.map +1 -1
- package/dist/base/table-view/meta.d.ts +8 -0
- package/dist/base/table-view/meta.js +25 -3
- package/dist/base/table-view/meta.js.map +1 -1
- package/dist/base/table-view/options.d.ts +90 -16
- package/dist/base/table-view/options.js.map +1 -1
- package/dist/base/table-view/realtime.d.ts +8 -0
- package/dist/base/table-view/realtime.js.map +1 -1
- package/dist/base/table-view/routes.js +5 -2
- package/dist/base/table-view/routes.js.map +1 -1
- package/dist/base/table-view/schema.d.ts +36 -30
- package/dist/base/table-view/schema.js +11 -57
- package/dist/base/table-view/schema.js.map +1 -1
- package/dist/base/table-view/source.d.ts +1 -1
- package/dist/base/table-view/source.js +4 -1
- package/dist/base/table-view/source.js.map +1 -1
- package/dist/base/table-view.d.ts +2 -2
- package/dist/base/table-view.js.map +1 -1
- package/dist/base/top-list-card.d.ts +5 -5
- package/dist/base/types/action-target.d.ts +77 -20
- package/dist/base/types/confirm-dialog.d.ts +5 -1
- package/dist/base/types/custom-button.d.ts +5 -3
- package/dist/base/types/field-config.d.ts +4 -4
- package/dist/base/types/guards.d.ts +10 -0
- package/dist/base/types/row-action.d.ts +6 -1
- package/dist/base/types/size.d.ts +3 -1
- package/dist/client-ip.d.ts +6 -2
- package/dist/client-ip.js +4 -2
- package/dist/client-ip.js.map +1 -1
- package/dist/component.d.ts +8 -0
- package/dist/component.js.map +1 -1
- package/dist/data-controllers/members.d.ts +0 -3
- package/dist/data-controllers/members.js +30 -8
- package/dist/data-controllers/members.js.map +1 -1
- package/dist/db/internal/tenantLifecycle.model.js +5 -2
- package/dist/db/internal/tenantLifecycle.model.js.map +1 -1
- package/dist/guards.js +2 -3
- package/dist/guards.js.map +1 -1
- package/dist/hooks.d.ts +41 -5
- package/dist/hooks.js +5 -1
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/invite-membership.d.ts +9 -0
- package/dist/internal/invite-membership.js +25 -7
- package/dist/internal/invite-membership.js.map +1 -1
- package/dist/internal/member-changes.d.ts +24 -0
- package/dist/internal/member-changes.js +66 -0
- package/dist/internal/member-changes.js.map +1 -0
- package/dist/internal/tenant-ownership.d.ts +4 -1
- package/dist/internal/tenant-ownership.js +4 -10
- package/dist/internal/tenant-ownership.js.map +1 -1
- package/dist/invite-extensions.d.ts +0 -9
- package/dist/invite-extensions.js +1 -36
- package/dist/invite-extensions.js.map +1 -1
- package/dist/invite-resolution.js +6 -2
- package/dist/invite-resolution.js.map +1 -1
- package/dist/invites.js +36 -22
- package/dist/invites.js.map +1 -1
- package/dist/layout-banners.d.ts +40 -4
- package/dist/layout-banners.js +25 -3
- package/dist/layout-banners.js.map +1 -1
- package/dist/notifications/internal.d.ts +2 -2
- package/dist/notifications/internal.js.map +1 -1
- package/dist/notifications/sendable.d.ts +3 -2
- package/dist/notifications/sendable.js +2 -1
- package/dist/notifications/sendable.js.map +1 -1
- package/dist/notifications/types.d.ts +9 -0
- package/dist/page/controllers.d.ts +44 -0
- package/dist/page/controllers.js +14 -1
- package/dist/page/controllers.js.map +1 -1
- package/dist/page/internal/layout-filter.d.ts +2 -1
- package/dist/page/internal/layout-filter.js +3 -1
- package/dist/page/internal/layout-filter.js.map +1 -1
- package/dist/page/internal/metadata-helpers.d.ts +7 -0
- package/dist/page/internal/metadata-helpers.js +13 -2
- package/dist/page/internal/metadata-helpers.js.map +1 -1
- package/dist/page/internal/page-registration.d.ts +2 -0
- package/dist/page/internal/page-registration.js.map +1 -1
- package/dist/page/layout-filter.js +31 -10
- package/dist/page/layout-filter.js.map +1 -1
- package/dist/page/metadata.js +29 -24
- package/dist/page/metadata.js.map +1 -1
- package/dist/page/types.d.ts +39 -4
- package/dist/page/types.js.map +1 -1
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -1
- package/dist/page.js.map +1 -1
- package/dist/permissions-resolver.d.ts +26 -3
- package/dist/permissions-resolver.js +2 -1
- package/dist/permissions-resolver.js.map +1 -1
- package/dist/permissions.d.ts +7 -3
- package/dist/permissions.js +8 -6
- package/dist/permissions.js.map +1 -1
- package/dist/realtime.d.ts +24 -1
- package/dist/realtime.js +7 -1
- package/dist/realtime.js.map +1 -1
- package/dist/request-tenant.d.ts +17 -13
- package/dist/request-tenant.js +54 -42
- package/dist/request-tenant.js.map +1 -1
- package/dist/tenant-members.d.ts +26 -0
- package/dist/tenant-members.js +29 -0
- package/dist/tenant-members.js.map +1 -0
- package/dist/tenant-ownership.d.ts +4 -1
- package/dist/tenant-ownership.js +12 -17
- package/dist/tenant-ownership.js.map +1 -1
- package/dist/tenant-scoped-model.d.ts +2 -1
- package/dist/tenant-scoped-model.js +4 -3
- package/dist/tenant-scoped-model.js.map +1 -1
- package/docs/1.core/10.layout-banners.md +57 -2
- package/docs/1.core/2.pages-and-modules.md +20 -11
- package/docs/1.core/4.permissions.md +34 -1
- package/docs/1.core/5.tenancy.md +37 -6
- package/docs/1.core/7.hooks.md +12 -2
- package/docs/1.core/8.realtime.md +24 -3
- package/docs/2.auth/2.authentication.md +3 -3
- package/docs/3.base/1.introduction.md +1 -0
- package/docs/3.base/10.display-blocks.md +20 -2
- package/docs/3.base/2.types.md +7 -5
- package/docs/3.base/3.forms.md +21 -6
- package/docs/3.base/4.tables.md +14 -9
- package/docs/3.base/5.charts-and-widgets.md +8 -4
- package/docs/3.base/6.layout.md +38 -1
- package/docs/3.base/8.data-types.md +14 -7
- package/docs/4.notifications/3.sending.md +4 -3
- package/docs/5.html-render/1.introduction.md +5 -7
- package/package.json +3 -7
|
@@ -17,7 +17,7 @@ Subscription is derived, not requested by the publisher:
|
|
|
17
17
|
|
|
18
18
|
1. A page — or a component on it — calls `RegisterPageTopic(pageId, topic)` at registration time. `pageId` is the page's `fullId` ([Pages and Modules](/docs/interfaces/core/pages-and-modules)), not its slug.
|
|
19
19
|
2. A client opens a realtime session and reports which page it is on. The backend checks that the user may access that page, then subscribes the session to every topic registered for that page id, and drops the topics that are no longer wanted.
|
|
20
|
-
3. `PublishMessage(topic, type)` delivers to the sessions currently subscribed to `topic
|
|
20
|
+
3. `PublishMessage(topic, type)` delivers to the sessions currently subscribed to `topic` — with a `tenantId`, only to those of that tenant (see [Tenant-scoped messages](#tenant-scoped-messages)).
|
|
21
21
|
|
|
22
22
|
So a publisher never addresses a page or a user. It addresses a topic, and the page-topic registry decides who is listening.
|
|
23
23
|
|
|
@@ -86,7 +86,7 @@ PublishMessage(
|
|
|
86
86
|
| --------- | ----------- |
|
|
87
87
|
| `topic` | The topic to publish on. Subscribers are the sessions on a page that registered this topic. |
|
|
88
88
|
| `type` | The event type carried with the message, for subscribers that distinguish several kinds of event on one topic. |
|
|
89
|
-
| `options` | Optional payload and
|
|
89
|
+
| `options` | Optional payload, actor and tenant. |
|
|
90
90
|
|
|
91
91
|
### `PublishMessageOptions`
|
|
92
92
|
|
|
@@ -94,12 +94,32 @@ PublishMessage(
|
|
|
94
94
|
| ------ | ---- | ----------- |
|
|
95
95
|
| `payload` | `Record<string, unknown>` | Data carried with the event. |
|
|
96
96
|
| `actorId` | `string` | Identifies who caused the event, so a client can ignore an event it triggered itself. |
|
|
97
|
+
| `tenantId` | `string` | The tenant the event is about: only the sessions of that tenant receive it. Omitted, the message is global and reaches every tenant. |
|
|
97
98
|
|
|
98
99
|
`PublishMessage` resolves once the message has been handed to the broker. It does not report how many sessions received it, and publishing on a topic no page registered is not an error — it simply reaches nobody.
|
|
99
100
|
|
|
101
|
+
### Tenant-scoped messages
|
|
102
|
+
|
|
103
|
+
Page topics are shared by every tenant: a page registers the same topic whichever workspace views it. A message published without a `tenantId` therefore reaches the sessions of **every** workspace on that page — each one refetches, and a paused live page counts the message in its "N new" banner. Publish a workspace's data with the tenant it belongs to:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { getRequestTenantId } from "@antelopejs/interface-dms/request-tenant";
|
|
107
|
+
|
|
108
|
+
await PublishMessage(CAMPAIGN_TOPIC, "campaign.sent", {
|
|
109
|
+
payload: { count: sent.length },
|
|
110
|
+
tenantId: getRequestTenantId(ctx), // or the tenant a job runs for
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A session is bound to the tenant of the request that opened it, and receives the messages of that tenant plus the global ones. When the browser switches workspace (or account), the client opens a new session in the new one, and the server ends a session whose page requests come from a workspace or account other than its own, so the previous workspace's messages stop with it. A session on the connection a browser's tabs share is ended alone: the other tabs' sessions on it stay. Keep messages without a tenant for what is the same for every workspace: a platform-wide setting, a catalogue shared by all tenants.
|
|
115
|
+
|
|
116
|
+
A session on a platform page (a page of a module, which only the platform owner opens) receives the messages of **every** tenant on that page's topics, whichever workspace the owner has open: such a page lists the data of all of them.
|
|
117
|
+
|
|
118
|
+
The table views of the [Base](/docs/interfaces/base/tables#realtime) domain publish their row changes and presence for the writer's tenant, or for every tenant when their rows live in the shared schema. Messages to one user (notifications) and the menu invalidations already name their audience in the topic.
|
|
119
|
+
|
|
100
120
|
## Subscribe from the Backend
|
|
101
121
|
|
|
102
|
-
`SubscribeMessage` receives every message published on a topic, whichever instance published it: the subscription rides the same broker as `PublishMessage`, so what one instance publishes reaches subscribers on every other.
|
|
122
|
+
`SubscribeMessage` receives every message published on a topic, whichever instance and whichever tenant published it (a handler serving one tenant filters on `message.tenantId` itself): the subscription rides the same broker as `PublishMessage`, so what one instance publishes reaches subscribers on every other.
|
|
103
123
|
|
|
104
124
|
```typescript
|
|
105
125
|
import { SubscribeMessage } from "@antelopejs/interface-dms/realtime";
|
|
@@ -132,6 +152,7 @@ type UnsubscribeMessage = () => void;
|
|
|
132
152
|
| `type` | `string` | Event type given to `PublishMessage`. |
|
|
133
153
|
| `payload` | `Record<string, unknown>` | Data carried with the event, when the publisher sent any. |
|
|
134
154
|
| `actorId` | `string` | Who caused the event, when the publisher named an actor. |
|
|
155
|
+
| `tenantId` | `string` | The tenant the message was published for; absent on a global message. |
|
|
135
156
|
| `ts` | `number` | Publication timestamp. |
|
|
136
157
|
|
|
137
158
|
Paired with `PublishMessage`, this turns the DMS's broker into one a module can use for realtime channels of its own — its own endpoint subscribes here and the DMS carries the messages — instead of bringing a second broker to do the same job.
|
|
@@ -7,7 +7,7 @@ description: "Authentication turns an incoming HTTP request into a `User` row."
|
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
Authentication turns an incoming HTTP request into a `User` row. The interface exposes one plain function, `authenticateRequestUser`, and four decorators
|
|
10
|
+
Authentication turns an incoming HTTP request into a `User` row. The interface exposes one plain function, `authenticateRequestUser`, and four decorators with the `CreateAuthDecorator` contract of `@antelopejs/interface-auth`: `AuthRawUser`, `AuthUser`, `IfAuthUser` and `AuthOwnerOnly`. Each of them, and `authenticateRequestUser`, binds the verified user and tenant to the request, which [`getRequestTenantId`](/docs/interfaces/core/tenancy#resolve-the-requests-tenant) then answers.
|
|
11
11
|
|
|
12
12
|
All of them read the bearer token from the request's `Authorization` header (`Authorization: Bearer <token>`) and run the same two steps:
|
|
13
13
|
|
|
@@ -113,7 +113,7 @@ export class MaintenanceController extends Controller("/api/maintenance") {
|
|
|
113
113
|
|
|
114
114
|
## Gate Every Route on a Controller
|
|
115
115
|
|
|
116
|
-
`AuthRawUser`, `AuthUser` and `IfAuthUser`
|
|
116
|
+
`AuthRawUser`, `AuthUser` and `IfAuthUser` follow the `CreateAuthDecorator` contract, so they apply to a parameter, to a property, or to the controller class itself. A property provider is a computed property of the controller, resolved on every request the controller handles whether or not the handler reads it — so a decorated property gates the whole controller and gives the handlers a typed `this.user`.
|
|
117
117
|
|
|
118
118
|
```typescript
|
|
119
119
|
import { Controller, Get, JSONBody, Post } from "@antelopejs/interface-api";
|
|
@@ -206,7 +206,7 @@ export const AuthBetaTester = MakeParameterAndPropertyDecorator(
|
|
|
206
206
|
|
|
207
207
|
This is how the DMS builds its tenant guards: `AuthTenantOwner` and `AuthTenantMember` in `@antelopejs/interface-dms/guards` both start from `authenticateRequestUser`, then resolve the request's tenant and check the caller's membership on top of it.
|
|
208
208
|
|
|
209
|
-
A provider is also the way to add a requirement without losing the built-in one. The `CreateAuthDecorator` contract lets a call site pass a replacement validator — `@AuthUser(callback)` — but that callback replaces the whole validation step, signature check included, and its return type is constrained to `boolean | User
|
|
209
|
+
A provider is also the way to add a requirement without losing the built-in one. The `CreateAuthDecorator` contract lets a call site pass a replacement validator — `@AuthUser(callback)` — but that callback replaces the whole validation step, signature check included, and its return type is constrained to `boolean | User`; such a call binds no tenant to the request. This interface exports no token-verification primitive, so a replacement validator cannot redo the signature check it removes.
|
|
210
210
|
|
|
211
211
|
## Decorator Reference
|
|
212
212
|
|
|
@@ -30,6 +30,7 @@ The DMS documentation carries task-shaped chapters on the same components, organ
|
|
|
30
30
|
| `top-list-card` | `TopListCard` | yes | [Charts and Widgets](/docs/interfaces/base/charts-and-widgets) |
|
|
31
31
|
| `period-selector` | `PeriodSelector` | yes | [Charts and Widgets](/docs/interfaces/base/charts-and-widgets) |
|
|
32
32
|
| `live-toggle` | `LiveToggle` | yes | [Display Blocks](/docs/interfaces/base/display-blocks#live-toggle) |
|
|
33
|
+
| `page-toolbar` | `PageToolbar`, `pageToolbar` | yes | [Display Blocks](/docs/interfaces/base/display-blocks#page-toolbar) |
|
|
33
34
|
| `grid` | `Grid`, `GridRow` | yes | [Layout and Containers](/docs/interfaces/base/layout) |
|
|
34
35
|
| `stack` | `HStack`, `VStack`, `Spacer` | yes | [Layout and Containers](/docs/interfaces/base/layout) |
|
|
35
36
|
| `tab` | `Tab` | yes | [Layout and Containers](/docs/interfaces/base/layout) |
|
|
@@ -72,7 +72,7 @@ A block that can drop its frame does so with `card?: boolean` (`KeyValueList`, `
|
|
|
72
72
|
|
|
73
73
|
| Props | Own options |
|
|
74
74
|
| ------------------- | --------------------------------------------------------- |
|
|
75
|
-
| `StatGroupProps` | `items`, `layout` (`StatGroupLayout`), `columns`, `label` |
|
|
75
|
+
| `StatGroupProps` | `items`, `layout` (`StatGroupLayout`), `columns`, `columnsFrom` (`StatGroupColumnsFrom`: `sm` / `md` / `lg` / `xl`, the group width from which all `columns` sit side by side), `label` |
|
|
76
76
|
| `KeyValueListProps` | `items`, `title`, `card`, `dense`, `columns`, `currency` |
|
|
77
77
|
| `NavCardGridProps` | `items`, `title`, `description`, `columns` |
|
|
78
78
|
|
|
@@ -127,10 +127,28 @@ Card({ title: "Connectors", count: 4 })
|
|
|
127
127
|
|
|
128
128
|
## Live Toggle
|
|
129
129
|
|
|
130
|
-
`LiveToggle(options?)` (subpath `live-toggle`, block type `LiveToggle`, component `dms-live-toggle-block`, group `data`) is the Live / Pause switch of a page declared with `live` (`PageLiveOptions` from `@antelopejs/interface-dms/page`). `LiveToggleProps` extends `BaseComponentProps`: `align` (`LIVE_TOGGLE_ALIGNS`: `left`, `center`, `right`; default `right`) `showUpdatedAt` (default `true`), the time since the page last read its data, counted in the browser, `variant` (`LIVE_TOGGLE_VARIANTS`: `segmented`, `pill`; default `segmented`) and `missedLabel`, an i18n key or text given `{count}` naming the missed events while paused. The state belongs to the page, not to the block; on a page without `live` the block renders nothing.
|
|
130
|
+
`LiveToggle(options?)` (subpath `live-toggle`, block type `LiveToggle`, component `dms-live-toggle-block`, group `data`) is the Live / Pause switch of a page declared with `live` (`PageLiveOptions` from `@antelopejs/interface-dms/page`). `LiveToggleProps` extends `BaseComponentProps`: `align` (`LIVE_TOGGLE_ALIGNS`: `left`, `center`, `right`; default `right`) `showUpdatedAt` (default `true`), the time since the page last read its data, counted in the browser, `variant` (`LIVE_TOGGLE_VARIANTS`: `segmented`, `pill`; default `segmented`), `size` (`LIVE_TOGGLE_SIZES`: `xs`, `sm`, `md`; default the holding `PageToolbar`'s, else `sm`) and `missedLabel`, an i18n key or text given `{count}` naming the missed events while paused. The state belongs to the page, not to the block; on a page without `live` the block renders nothing.
|
|
131
131
|
|
|
132
132
|
```typescript
|
|
133
133
|
import { LiveToggle } from "@antelopejs/interface-dms/base/live-toggle";
|
|
134
134
|
|
|
135
135
|
static toolbar = HStack().child("spacer", Spacer()).child("live", LiveToggle());
|
|
136
136
|
```
|
|
137
|
+
|
|
138
|
+
## Page Toolbar
|
|
139
|
+
|
|
140
|
+
`PageToolbar(options?)` (subpath `page-toolbar`, block type `PageToolbar`, component `dms-page-toolbar-block`, group `data`, a container) is the context bar of a page with a period or live data, first in its body. `PageToolbarProps` extends `BaseComponentProps`: `size` (`PAGE_TOOLBAR_SIZES`: `xs`, `sm`, `md`; default `sm`), the size of the `LiveToggle` and `PeriodSelector` it holds unless they set one, and `showUpdatedAt` and `refresh` (both default `true`), the "updated N s ago" label and the refresh button it shows where no `LiveToggle` is on screen — a page without `live` — counted from the page's last read. Its children render in one order whatever order they were added in: the other blocks, a spacer, the updated label or the `LiveToggle`, then the `PeriodSelector`.
|
|
141
|
+
|
|
142
|
+
`pageToolbar(options?)` builds one: `PageToolbarOptions` extends `PageToolbarProps` with `leading` (`Record<string, Component>`, the blocks on the left by child id), `live` (`LiveToggleProps | false`, child `live`, over `PAGE_TOOLBAR_LIVE_DEFAULTS`) and `period` (`PeriodSelectorProps | false`, child `period`, over `PAGE_TOOLBAR_PERIOD_DEFAULTS`: segmented `last-hour` / `last-24h` / `last-7-days` / `last-30-days`, `defaultPreset: "last-24h"`, `rangeLabel: "relative"`, right aligned). Unless `period` sets `comparisonControl`, it is `"menu"` when the period offers more than one comparison (`none` included; the selector's default list when `comparisons` is unset), on `defaultComparison` or else the first one besides `none`, and `"hidden"` with a single comparison or none.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { pageToolbar } from "@antelopejs/interface-dms/base/page-toolbar";
|
|
146
|
+
|
|
147
|
+
static toolbar = pageToolbar({
|
|
148
|
+
leading: { scope: scopeChip },
|
|
149
|
+
live: { missedLabel: "$api.live.new_requests" },
|
|
150
|
+
period: { id: "api", rememberKey: "api", comparisons: ["previous-period"] },
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A module's Vue component may also draw the toolbar itself, around controls of its own, as `<DmsPageToolbarBlock>` holding a `<DmsLiveToggleBlock>`: outside the page's block tree, neither needs a `componentId` or `pageId`.
|
package/docs/3.base/2.types.md
CHANGED
|
@@ -90,7 +90,7 @@ import {
|
|
|
90
90
|
|
|
91
91
|
`Size` maps readable names onto the scale tokens: `tiny` → `"xs"`, `small` → `"sm"`, `medium` → `"md"`, `large` → `"lg"`, `huge` → `"xl"`. `SizeValue` widens it the same way `ColorValue` widens `Color`.
|
|
92
92
|
|
|
93
|
-
`ModalSize` is a separate, wider union
|
|
93
|
+
`ModalSize` is a separate, wider union for modal and drawer action targets: `"sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "full"`: a modal's width, and the width of a side drawer given that size, `full` the whole window.
|
|
94
94
|
|
|
95
95
|
## Buttons
|
|
96
96
|
|
|
@@ -145,9 +145,9 @@ import type { ActionTarget } from "@antelopejs/interface-dms/base/types/action-t
|
|
|
145
145
|
|
|
146
146
|
| `type` | Fields |
|
|
147
147
|
| --- | --- |
|
|
148
|
-
| `"drawer"` | `component: Component`, `title?`, `description?`, `direction?: DrawerDirection` |
|
|
148
|
+
| `"drawer"` | `component: Component`, `title?`, `description?`, `direction?: DrawerDirection`, plus `DrawerTargetOptions`: `size?: ModalSize`, `rowHeader?`, `rowTitle?`, `positionLabel?`, `deepLink?`, `notFound?` |
|
|
149
149
|
| `"modal"` | `component: Component`, `size?: ModalSize`, `title?`, `description?` |
|
|
150
|
-
| `"sidePanel"` | `component: Component`, plus `SidePanelTargetOptions`: `title?`, `rowTitle?`, `positionLabel?`, `deepLink?`, `notFound?` |
|
|
150
|
+
| `"sidePanel"` | `component: Component`, plus `SidePanelTargetOptions`: `title?`, `rowTitle?`, `positionLabel?`, `deepLink?`, `notFound?` — for global, page-independent panels; a detail panel is a `drawer` |
|
|
151
151
|
| `"page"` | `url: string`: the page URL's `{{params.X}}` / `{{query.X}}` (a query pair without a value left out), then on a row action the row's `{{row.field}}`; see [Template tokens](/docs/building/pages-and-components#template-tokens) |
|
|
152
152
|
| `"external"` | `url: string`, `newTab?: boolean`; the same tokens as `page` |
|
|
153
153
|
| `"download"` | `url: string`, `filename?: string` (`DownloadActionTarget`); the same tokens as `page`, the file name taking them as written. Fetched with the user's session and saved under `filename`, else the server's `Content-Disposition` name, else the URL's last segment; a failure is toasted |
|
|
@@ -155,9 +155,11 @@ import type { ActionTarget } from "@antelopejs/interface-dms/base/types/action-t
|
|
|
155
155
|
| `"api"` | `url: string`, `method?: "GET" \| "POST" \| "PUT" \| "DELETE" \| "PATCH"`, `body?`, `copy?`, `successMessage: string`; the same tokens as `page` |
|
|
156
156
|
| `"exportJob"` | `url: string`, `method?: "GET" \| "POST"`, `statusUrl?`, `downloadUrl?`, `labels?`; the same tokens as `page`, plus `{{job.id}}` in `statusUrl` and `downloadUrl` |
|
|
157
157
|
|
|
158
|
-
`SidePanelTargetOptions` sets the header the DMS draws above a `sidePanel` target's component: `title` (the action's label by default; `$`: an i18n key), `rowTitle`, the title while a row is open, an i18n key or a literal receiving the row's fields (`"{method} {path}"`), `positionLabel`, an i18n key receiving `{ index }` (from 1) and `{ total }` for the row's position among the rows the table
|
|
158
|
+
`SidePanelTargetOptions` sets the header the DMS draws above a `sidePanel` target's component: `title` (the action's label by default; `$`: an i18n key), `rowTitle`, the title while a row is open, an i18n key or a literal receiving the row's fields (`"{method} {path}"`), `positionLabel`, an i18n key receiving `{ index }` (from 1) and `{ total }` for the row's position among all the rows the table lists, its count rather than the page loaded (the DMS's "{index} of {total}" by default), and `deepLink`, which keeps the open row in the URL like the action's own `deepLink` — on by default for a side panel, `false` opting out (the action's own `deepLink` wins when set), and `notFound` (`SidePanelNotFoundTexts`: `title?`, `description?`), the state the panel draws with a close button when the row a link names answers 404. The panel is non-modal and docked on the right, one at a time with a module's side panels. It is meant for global, page-independent panels (an assistant dock): a detail or record panel opens in a `drawer`.
|
|
159
159
|
|
|
160
|
-
`
|
|
160
|
+
`DrawerTargetOptions` gives a `drawer` target opened on a row the same options, so a detail panel moves from `sidePanel` to `drawer` without losing any: `rowTitle`, `positionLabel` and `notFound` as above; `rowHeader`, the DMS drawing the row header (the title naming the row, its position, previous and next buttons next to the close button) — on when `rowTitle` or `positionLabel` is set, `false` leaving the drawer's own title, `true` drawing it with the defaults; `deepLink`, off by default for a drawer (the action's own `deepLink` wins when set); and `size` (`ModalSize`), the width a `left` / `right` drawer opens at from `lg`, a modal's width at that size. J and K step through the rows in place whatever the header. A drawer shows its not-found state only once its target sets `notFound`; otherwise a deep link to a missing row is a toast, as before.
|
|
161
|
+
|
|
162
|
+
`DrawerDirection` is `"top" | "right" | "bottom" | "left"`, the edge the drawer slides in from: `bottom` by default, while `left` and `right` open a side sheet spanning the window's height, 600px wide from `lg` without a `size` (capped to the window) and 440px below it. From `lg` the user resizes every drawer by its inner edge (width or height), and the drawer opens again at the size it was left at.
|
|
161
163
|
|
|
162
164
|
A target carries no confirmation: the action does (`CustomButton.confirm`, `CustomRowAction.confirm`, `RowActionConfig.confirm`), with one type, `ActionConfirm = ConfirmDialog | ConfirmDialogFrom` (see below). The `labels` object on `exportJob` accepts `title`, `exporting`, `downloading`, `successTitle`, `successMessage`, `errorTitle` and `retry`, all optional strings.
|
|
163
165
|
|
package/docs/3.base/3.forms.md
CHANGED
|
@@ -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,8 +112,10 @@ 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. |
|
|
118
120
|
|
|
119
121
|
`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.
|
|
@@ -275,6 +277,8 @@ const priority = FormComponents.InputSelect({
|
|
|
275
277
|
| `File(options?)` | `FileOptions` |
|
|
276
278
|
| `Image(options?)` | `ImageOptions` |
|
|
277
279
|
|
|
280
|
+
`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.
|
|
281
|
+
|
|
278
282
|
The option interfaces live in the same namespace. The ones with more than a placeholder:
|
|
279
283
|
|
|
280
284
|
| Interface | Fields |
|
|
@@ -282,9 +286,9 @@ The option interfaces live in the same namespace. The ones with more than a plac
|
|
|
282
286
|
| `SelectOption` | `label: string`, `value: string \| number`, `disabled?`, `icon?`, `iconColor?`, `textColor?` |
|
|
283
287
|
| `SelectOptions` | `items: SelectOption[]`, `placeholder?`, `multiple?`, `deselectable?` |
|
|
284
288
|
| `RadioGroupOptions` | `items: SelectOption[]`, `orientation?: AxeOrientation` |
|
|
285
|
-
| `InputTextOptions` | `placeholder?`, `maxLength?`, `minLength?`, `showCount
|
|
289
|
+
| `InputTextOptions` | `placeholder?`, `maxLength?`, `minLength?`, `showCount?`, `width?: InputWidth` |
|
|
286
290
|
| `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) |
|
|
291
|
+
| `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
292
|
| `InputTimeOptions` | `min?`, `max?`, `placeholder?` |
|
|
289
293
|
| `InputPhoneOptions` | `placeholder?`, `requiredPrefix?` |
|
|
290
294
|
| `InputPasswordOptions` | `placeholder?`, `minLength?`, `confirmPassword?`, `confirmPlaceholder?` |
|
|
@@ -316,14 +320,16 @@ The option interfaces live in the same namespace. The ones with more than a plac
|
|
|
316
320
|
| `FormFunctions.SET_FIELD_DISABLED` | `"DmsComponent.Form.SetFieldDisabled"` |
|
|
317
321
|
| `FormFunctions.SET_FIELD_HIDDEN` | `"DmsComponent.Form.SetFieldHidden"` |
|
|
318
322
|
| `FormFunctions.SET_FIELD_REQUIRED` | `"DmsComponent.Form.SetFieldRequired"` |
|
|
323
|
+
| `FormFunctions.SET_FIELD_VALUE` | `"DmsComponent.Form.SetFieldValue"` |
|
|
319
324
|
|
|
320
|
-
|
|
325
|
+
Each function registers their parameter shape in `WatchFunctionParamMap`, so `.watch()` type-checks the `params` object:
|
|
321
326
|
|
|
322
327
|
| Function | Params |
|
|
323
328
|
| --- | --- |
|
|
324
329
|
| `SET_FIELD_DISABLED` | `{ targetField: string; setDisabled: boolean }` |
|
|
325
330
|
| `SET_FIELD_HIDDEN` | `{ targetField: string; setHidden: boolean }` |
|
|
326
331
|
| `SET_FIELD_REQUIRED` | `{ targetField: string; setRequired: boolean }` |
|
|
332
|
+
| `SET_FIELD_VALUE` | `SetFieldValueParams`: `{ targetField: string; value?: unknown; valueMap?: Record<string, unknown> }` |
|
|
327
333
|
|
|
328
334
|
```typescript
|
|
329
335
|
import { Form, FormEvents, FormFunctions } from "@antelopejs/interface-dms/base/form";
|
|
@@ -340,7 +346,16 @@ const form = Form({
|
|
|
340
346
|
});
|
|
341
347
|
```
|
|
342
348
|
|
|
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.
|
|
349
|
+
`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.
|
|
350
|
+
|
|
351
|
+
`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.
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
form.watch(FormEvents.FIELD_CHANGE, FormFunctions.SET_FIELD_VALUE, {
|
|
355
|
+
params: { targetField: "probability", valueMap: { won: 100, lost: 0 } },
|
|
356
|
+
onParam: { key: "fieldId", value: "stage" },
|
|
357
|
+
});
|
|
358
|
+
```
|
|
344
359
|
|
|
345
360
|
`.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
361
|
|
package/docs/3.base/4.tables.md
CHANGED
|
@@ -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. |
|
|
@@ -139,7 +139,7 @@ These are not the plain data-api defaults. Every route asserts tenant access and
|
|
|
139
139
|
| `size` | `number` | Default width of the grid column in px (150 when omitted). |
|
|
140
140
|
| `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
141
|
|
|
142
|
-
A read-only
|
|
142
|
+
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.
|
|
143
143
|
|
|
144
144
|
### Column Groups
|
|
145
145
|
|
|
@@ -230,7 +230,7 @@ Typing `TableViewOptions` by `InstanceType<T>` is what makes row-action rules fi
|
|
|
230
230
|
| `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
231
|
| `searchPlaceholder` | `string` | Placeholder of the search field, also its accessible name. |
|
|
232
232
|
| `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. |
|
|
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. 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
234
|
| `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
235
|
| `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
236
|
| `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 +241,10 @@ Typing `TableViewOptions` by `InstanceType<T>` is what makes row-action rules fi
|
|
|
241
241
|
| `rowActions` | `TableViewRowActionOptions<T>` | Built-in and custom row actions. |
|
|
242
242
|
| `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
243
|
| `customButtons` | `CustomButton[]` | Toolbar buttons; see [Shared Types](/docs/interfaces/base/types). |
|
|
244
|
+
| `forms` | `Partial<Record<"new" \| "edit" \| "view", TableViewFormSetup>>` | A callback per generated form, `(form: FormBuilder) => void`, receiving its builder before the table view embeds it: what it declares (`.watch()`, a field shown, hidden or required as another changes) holds in a drawer, a modal or a form page alike. |
|
|
244
245
|
| `formContainer` | `FormContainer` | Where the `new` / `edit` / `view` forms open. |
|
|
245
246
|
| `archiveMode` | `boolean` | Replace delete with archive/restore. Requires `@ArchiveField`. |
|
|
247
|
+
| `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
248
|
| `defaultFilters` | `Array<{ accessorKey: string; value?: string; mode: string }>` | Filters applied when the table opens. |
|
|
247
249
|
| `defaultSort` | `{ field: string; desc?: boolean }` | Sort applied when the viewer has no stored preference, and the one a cleared sort goes back to. |
|
|
248
250
|
| `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 +254,15 @@ Typing `TableViewOptions` by `InstanceType<T>` is what makes row-action rules fi
|
|
|
252
254
|
| `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
255
|
| `realtime` | `boolean` | Set to `false` to disable realtime broadcast and presence for this resource. Realtime is on by default. |
|
|
254
256
|
| `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"
|
|
257
|
+
| `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
258
|
| `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
259
|
| `kanban` | `KanbanOptions` | Enable the kanban display. |
|
|
258
260
|
| `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
261
|
| `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. |
|
|
262
|
+
| `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
263
|
| `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
264
|
| `defaultDisplay` | `string` | Display shown when the viewer has no stored preference. Defaults to `"table"`. |
|
|
265
|
+
| `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
266
|
| `guards` | `TableViewGuards<T>` | Server-side guards run before mutating actions. |
|
|
264
267
|
| `bypassTenantAccessGate` | `boolean` | Skip the tenant access gate on every data route of this table view. |
|
|
265
268
|
|
|
@@ -269,7 +272,7 @@ Typing `TableViewOptions` by `InstanceType<T>` is what makes row-action rules fi
|
|
|
269
272
|
TableView.fromSource(options: TableViewSourceOptions): ComponentBuilder<TableViewOptionsSerialized>;
|
|
270
273
|
```
|
|
271
274
|
|
|
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}}
|
|
275
|
+
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
276
|
|
|
274
277
|
### Reaching a Table Behind a Tenant Gate
|
|
275
278
|
|
|
@@ -358,7 +361,7 @@ A `new` or `edit` `ResourceForm` block over the controller submits to the same r
|
|
|
358
361
|
|
|
359
362
|
`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
363
|
|
|
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`).
|
|
364
|
+
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
365
|
|
|
363
366
|
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
367
|
|
|
@@ -442,7 +445,7 @@ static table = TableView(invoiceDataAPI, {
|
|
|
442
445
|
|
|
443
446
|
`KanbanOptionsSerialized` is `KanbanOptions` plus the serialized `card` (`TableViewCardOptionsSerialized`).
|
|
444
447
|
|
|
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).
|
|
448
|
+
`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
449
|
|
|
447
450
|
## Generated Forms
|
|
448
451
|
|
|
@@ -516,7 +519,9 @@ tableViewRowTopic("/api/orders"); // "tableview:row:/api/orders"
|
|
|
516
519
|
tableViewPresenceTopic("/api/orders"); // "tableview:presence:/api/orders"
|
|
517
520
|
```
|
|
518
521
|
|
|
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
|
|
522
|
+
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`.
|
|
523
|
+
|
|
524
|
+
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
525
|
|
|
521
526
|
Two functions let other modules observe those mutations:
|
|
522
527
|
|
|
@@ -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. |
|
package/docs/3.base/6.layout.md
CHANGED
|
@@ -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` | `PageRecordSource` | The record the page shows (`{ fetchUrl }`), which header buttons act on — 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
|
|
279
|
+
An `availability` receives the page URL's query as `context.query` (one value per name; caller-supplied, so check it like any input) and its route parameters as `context.params` (`/projects/project/p1` on a page registered at `/projects/project/:id` is `{ id: "p1" }`). The header asks again whenever the query or the route parameters change — 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
280
|
|
|
280
281
|
```typescript
|
|
281
282
|
DefaultLayout({
|
|
@@ -301,6 +302,26 @@ 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) declares it with `record: { fetchUrl }` — typically its controller's `get` route, `{{params.X}}` and `{{query.X}}` filled in from the page URL. The browser reads it for the header, and a button's target names its fields as `{{record.X}}`, 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 disabled while the record loads, and left out of a page whose record cannot be read (not found, refused). The other buttons never wait for it:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
DefaultLayout({
|
|
311
|
+
record: { fetchUrl: "/api/sites/get?id={{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
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The record is read like a block's `fetchUrl` (`useApiData`, keyed on the URL): a block of the page reading the same URL — the `ResourceForm` or `KeyValueList` showing the record — shares the one request, and a server render hands it to the browser with the page. Its fields only shape the buttons; the route behind each one still checks the record on its own.
|
|
324
|
+
|
|
304
325
|
#### Hidden or disabled
|
|
305
326
|
|
|
306
327
|
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 +341,22 @@ DefaultLayout({
|
|
|
320
341
|
});
|
|
321
342
|
```
|
|
322
343
|
|
|
344
|
+
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:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
DefaultLayout({
|
|
348
|
+
headerActions: [
|
|
349
|
+
{
|
|
350
|
+
id: "edit",
|
|
351
|
+
label: "$projects.edit",
|
|
352
|
+
target: { type: "page", url: "/projects/project/{{params.id}}/edit" },
|
|
353
|
+
availability: async ({ params }) =>
|
|
354
|
+
(await projectExists(params?.id)) ? undefined : { hidden: true },
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
323
360
|
Hiding only shapes the UI: the route behind the button must still refuse the operation on its own.
|
|
324
361
|
|
|
325
362
|
#### Ordering the header
|