@antelopejs/interface-dms 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +70 -68
- package/dist/auth/db/models/sessions.model.d.ts +1 -0
- package/dist/auth/db/models/userExternalIdentities.model.d.ts +1 -0
- package/dist/auth/db/models/users.model.d.ts +1 -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/form-block-schema.d.ts +24 -24
- package/dist/base/live-toggle.d.ts +2 -2
- package/dist/base/period-selector.d.ts +2 -2
- package/dist/base/section.d.ts +2 -2
- package/dist/base/tab.d.ts +4 -4
- package/dist/base/table-view/schema.d.ts +4 -4
- package/dist/db/internal/exportJobs.model.d.ts +1 -0
- package/dist/db/internal/inviteResolutions.model.d.ts +1 -0
- package/dist/db/internal/tenantLifecycle.model.d.ts +1 -0
- package/dist/db/models/roles.model.d.ts +1 -0
- package/dist/db/models/tenantMembers.model.d.ts +1 -0
- package/dist/db/models/tenants.model.d.ts +1 -0
- package/dist/db/models/user_invites.model.d.ts +1 -0
- package/docs/.navigation.yml +2 -0
- package/docs/1.core/.navigation.yml +2 -0
- package/docs/1.core/1.introduction.md +169 -0
- package/docs/1.core/10.layout-banners.md +133 -0
- package/docs/1.core/2.pages-and-modules.md +647 -0
- package/docs/1.core/3.components.md +287 -0
- package/docs/1.core/4.permissions.md +279 -0
- package/docs/1.core/5.tenancy.md +356 -0
- package/docs/1.core/6.invites.md +239 -0
- package/docs/1.core/7.hooks.md +354 -0
- package/docs/1.core/8.realtime.md +147 -0
- package/docs/1.core/9.quick-actions.md +115 -0
- package/docs/2.auth/.navigation.yml +2 -0
- package/docs/2.auth/1.introduction.md +75 -0
- package/docs/2.auth/2.authentication.md +222 -0
- package/docs/2.auth/3.tokens-and-sessions.md +352 -0
- package/docs/2.auth/4.users.md +256 -0
- package/docs/2.auth/5.external-identities.md +122 -0
- package/docs/3.base/.navigation.yml +2 -0
- package/docs/3.base/1.introduction.md +202 -0
- package/docs/3.base/10.display-blocks.md +136 -0
- package/docs/3.base/2.types.md +402 -0
- package/docs/3.base/3.forms.md +349 -0
- package/docs/3.base/4.tables.md +608 -0
- package/docs/3.base/5.charts-and-widgets.md +466 -0
- package/docs/3.base/6.layout.md +431 -0
- package/docs/3.base/8.data-types.md +361 -0
- package/docs/3.base/9.helpers.md +414 -0
- package/docs/4.notifications/.navigation.yml +2 -0
- package/docs/4.notifications/1.introduction.md +79 -0
- package/docs/4.notifications/2.categories-and-subjects.md +212 -0
- package/docs/4.notifications/3.sending.md +283 -0
- package/docs/5.html-render/.navigation.yml +2 -0
- package/docs/5.html-render/1.introduction.md +372 -0
- package/docs/5.html-render/2.built-in-emails.md +107 -0
- package/docs/index.md +100 -0
- package/docs/tsconfig.json +11 -0
- package/package.json +24 -18
|
@@ -141,13 +141,13 @@ export declare const PeriodSelectorSchema: z.ZodObject<{
|
|
|
141
141
|
size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
|
|
142
142
|
variant?: "default" | "segmented" | undefined;
|
|
143
143
|
rememberKey?: string | undefined;
|
|
144
|
+
align?: "left" | "right" | "center" | undefined;
|
|
144
145
|
defaultPreset?: "custom" | "all" | "last-hour" | "last-24h" | "today" | "yesterday" | "last-7-days" | "last-30-days" | "last-90-days" | "this-month" | "last-month" | "this-quarter" | "last-quarter" | "ytd" | "last-year" | undefined;
|
|
145
146
|
defaultComparison?: "custom" | "none" | "previous-period" | "previous-year" | undefined;
|
|
146
147
|
presets?: ("custom" | "all" | "last-hour" | "last-24h" | "today" | "yesterday" | "last-7-days" | "last-30-days" | "last-90-days" | "this-month" | "last-month" | "this-quarter" | "last-quarter" | "ytd" | "last-year")[] | undefined;
|
|
147
148
|
comparisons?: ("custom" | "none" | "previous-period" | "previous-year")[] | undefined;
|
|
148
149
|
presetLabels?: Partial<Record<"custom" | "all" | "last-hour" | "last-24h" | "today" | "yesterday" | "last-7-days" | "last-30-days" | "last-90-days" | "this-month" | "last-month" | "this-quarter" | "last-quarter" | "ytd" | "last-year", string>> | undefined;
|
|
149
150
|
comparisonLabels?: Partial<Record<"custom" | "none" | "previous-period" | "previous-year", string>> | undefined;
|
|
150
|
-
align?: "left" | "right" | "center" | undefined;
|
|
151
151
|
showRangeLabel?: boolean | undefined;
|
|
152
152
|
rangeLabel?: "relative" | "absolute" | undefined;
|
|
153
153
|
comparisonControl?: "hidden" | "menu" | "toggle" | undefined;
|
|
@@ -157,13 +157,13 @@ export declare const PeriodSelectorSchema: z.ZodObject<{
|
|
|
157
157
|
id?: string | undefined;
|
|
158
158
|
variant?: "default" | "segmented" | undefined;
|
|
159
159
|
rememberKey?: string | undefined;
|
|
160
|
+
align?: "left" | "right" | "center" | undefined;
|
|
160
161
|
defaultPreset?: "custom" | "all" | "last-hour" | "last-24h" | "today" | "yesterday" | "last-7-days" | "last-30-days" | "last-90-days" | "this-month" | "last-month" | "this-quarter" | "last-quarter" | "ytd" | "last-year" | undefined;
|
|
161
162
|
defaultComparison?: "custom" | "none" | "previous-period" | "previous-year" | undefined;
|
|
162
163
|
presets?: ("custom" | "all" | "last-hour" | "last-24h" | "today" | "yesterday" | "last-7-days" | "last-30-days" | "last-90-days" | "this-month" | "last-month" | "this-quarter" | "last-quarter" | "ytd" | "last-year")[] | undefined;
|
|
163
164
|
comparisons?: ("custom" | "none" | "previous-period" | "previous-year")[] | undefined;
|
|
164
165
|
presetLabels?: Partial<Record<"custom" | "all" | "last-hour" | "last-24h" | "today" | "yesterday" | "last-7-days" | "last-30-days" | "last-90-days" | "this-month" | "last-month" | "this-quarter" | "last-quarter" | "ytd" | "last-year", string>> | undefined;
|
|
165
166
|
comparisonLabels?: Partial<Record<"custom" | "none" | "previous-period" | "previous-year", string>> | undefined;
|
|
166
|
-
align?: "left" | "right" | "center" | undefined;
|
|
167
167
|
showRangeLabel?: boolean | undefined;
|
|
168
168
|
rangeLabel?: "relative" | "absolute" | undefined;
|
|
169
169
|
comparisonControl?: "hidden" | "menu" | "toggle" | undefined;
|
package/dist/base/section.d.ts
CHANGED
|
@@ -91,15 +91,15 @@ export declare const FieldRowSchema: z.ZodObject<{
|
|
|
91
91
|
required: z.ZodOptional<z.ZodBoolean>;
|
|
92
92
|
disabled: z.ZodOptional<z.ZodBoolean>;
|
|
93
93
|
}, "strip", z.ZodTypeAny, {
|
|
94
|
+
disabled?: boolean | undefined;
|
|
94
95
|
required?: boolean | undefined;
|
|
95
96
|
description?: string | undefined;
|
|
96
97
|
label?: string | undefined;
|
|
97
|
-
disabled?: boolean | undefined;
|
|
98
98
|
layout?: "stack" | "inline" | "form" | undefined;
|
|
99
99
|
}, {
|
|
100
|
+
disabled?: boolean | undefined;
|
|
100
101
|
required?: boolean | undefined;
|
|
101
102
|
description?: string | undefined;
|
|
102
103
|
label?: string | undefined;
|
|
103
|
-
disabled?: boolean | undefined;
|
|
104
104
|
layout?: "stack" | "inline" | "form" | undefined;
|
|
105
105
|
}>;
|
package/dist/base/tab.d.ts
CHANGED
|
@@ -132,8 +132,8 @@ export declare const TabSchema: z.ZodObject<{
|
|
|
132
132
|
}, "strip", z.ZodTypeAny, {
|
|
133
133
|
label: string;
|
|
134
134
|
slot: string;
|
|
135
|
-
icon?: string | undefined;
|
|
136
135
|
disabled?: boolean | undefined;
|
|
136
|
+
icon?: string | undefined;
|
|
137
137
|
avatar?: {
|
|
138
138
|
size?: Size | undefined;
|
|
139
139
|
alt?: string | undefined;
|
|
@@ -149,8 +149,8 @@ export declare const TabSchema: z.ZodObject<{
|
|
|
149
149
|
}, {
|
|
150
150
|
label: string;
|
|
151
151
|
slot: string;
|
|
152
|
-
icon?: string | undefined;
|
|
153
152
|
disabled?: boolean | undefined;
|
|
153
|
+
icon?: string | undefined;
|
|
154
154
|
avatar?: {
|
|
155
155
|
size?: Size | undefined;
|
|
156
156
|
alt?: string | undefined;
|
|
@@ -176,8 +176,8 @@ export declare const TabSchema: z.ZodObject<{
|
|
|
176
176
|
items: {
|
|
177
177
|
label: string;
|
|
178
178
|
slot: string;
|
|
179
|
-
icon?: string | undefined;
|
|
180
179
|
disabled?: boolean | undefined;
|
|
180
|
+
icon?: string | undefined;
|
|
181
181
|
avatar?: {
|
|
182
182
|
size?: Size | undefined;
|
|
183
183
|
alt?: string | undefined;
|
|
@@ -205,8 +205,8 @@ export declare const TabSchema: z.ZodObject<{
|
|
|
205
205
|
items: {
|
|
206
206
|
label: string;
|
|
207
207
|
slot: string;
|
|
208
|
-
icon?: string | undefined;
|
|
209
208
|
disabled?: boolean | undefined;
|
|
209
|
+
icon?: string | undefined;
|
|
210
210
|
avatar?: {
|
|
211
211
|
size?: Size | undefined;
|
|
212
212
|
alt?: string | undefined;
|
|
@@ -839,17 +839,17 @@ export declare const TableViewSchema: z.ZodObject<{
|
|
|
839
839
|
whileSorted: z.ZodOptional<z.ZodBoolean>;
|
|
840
840
|
missedLabel: z.ZodOptional<z.ZodString>;
|
|
841
841
|
}, "strip", z.ZodTypeAny, {
|
|
842
|
+
missedLabel?: string | undefined;
|
|
842
843
|
whileRowOpen?: boolean | undefined;
|
|
843
844
|
whileScrolled?: boolean | undefined;
|
|
844
845
|
whileSearching?: boolean | undefined;
|
|
845
846
|
whileSorted?: boolean | undefined;
|
|
846
|
-
missedLabel?: string | undefined;
|
|
847
847
|
}, {
|
|
848
|
+
missedLabel?: string | undefined;
|
|
848
849
|
whileRowOpen?: boolean | undefined;
|
|
849
850
|
whileScrolled?: boolean | undefined;
|
|
850
851
|
whileSearching?: boolean | undefined;
|
|
851
852
|
whileSorted?: boolean | undefined;
|
|
852
|
-
missedLabel?: string | undefined;
|
|
853
853
|
}>]>>;
|
|
854
854
|
kanban: z.ZodOptional<z.ZodObject<{
|
|
855
855
|
groupByField: z.ZodString;
|
|
@@ -1137,11 +1137,11 @@ export declare const TableViewSchema: z.ZodObject<{
|
|
|
1137
1137
|
strictRuleValidation?: boolean | undefined;
|
|
1138
1138
|
realtimeTopic?: string | string[] | undefined;
|
|
1139
1139
|
liveHold?: boolean | {
|
|
1140
|
+
missedLabel?: string | undefined;
|
|
1140
1141
|
whileRowOpen?: boolean | undefined;
|
|
1141
1142
|
whileScrolled?: boolean | undefined;
|
|
1142
1143
|
whileSearching?: boolean | undefined;
|
|
1143
1144
|
whileSorted?: boolean | undefined;
|
|
1144
|
-
missedLabel?: string | undefined;
|
|
1145
1145
|
} | undefined;
|
|
1146
1146
|
defaultDisplay?: string | undefined;
|
|
1147
1147
|
guards?: TableViewGuards<Record<string, unknown>> | undefined;
|
|
@@ -1352,11 +1352,11 @@ export declare const TableViewSchema: z.ZodObject<{
|
|
|
1352
1352
|
realtime?: boolean | undefined;
|
|
1353
1353
|
realtimeTopic?: string | string[] | undefined;
|
|
1354
1354
|
liveHold?: boolean | {
|
|
1355
|
+
missedLabel?: string | undefined;
|
|
1355
1356
|
whileRowOpen?: boolean | undefined;
|
|
1356
1357
|
whileScrolled?: boolean | undefined;
|
|
1357
1358
|
whileSearching?: boolean | undefined;
|
|
1358
1359
|
whileSorted?: boolean | undefined;
|
|
1359
|
-
missedLabel?: string | undefined;
|
|
1360
1360
|
} | undefined;
|
|
1361
1361
|
defaultDisplay?: string | undefined;
|
|
1362
1362
|
guards?: TableViewGuards<Record<string, unknown>> | undefined;
|
|
@@ -24,6 +24,7 @@ declare const ExportJobModel_base: {
|
|
|
24
24
|
delete(id: string): Promise<number>;
|
|
25
25
|
};
|
|
26
26
|
readonly schemaName: string;
|
|
27
|
+
readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<ExportJob>;
|
|
27
28
|
fromPlainData(obj: any): ExportJob;
|
|
28
29
|
fromDatabase(obj: any): ExportJob | undefined;
|
|
29
30
|
toDatabase(obj: any): Record<string, any>;
|
|
@@ -13,6 +13,7 @@ declare const InviteResolutionsModel_base: {
|
|
|
13
13
|
delete(id: string): Promise<number>;
|
|
14
14
|
};
|
|
15
15
|
readonly schemaName: string;
|
|
16
|
+
readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<InviteResolution>;
|
|
16
17
|
fromPlainData(obj: any): InviteResolution;
|
|
17
18
|
fromDatabase(obj: any): InviteResolution | undefined;
|
|
18
19
|
toDatabase(obj: any): Record<string, any>;
|
|
@@ -12,6 +12,7 @@ declare const TenantLifecycleModel_base: {
|
|
|
12
12
|
delete(id: string): Promise<number>;
|
|
13
13
|
};
|
|
14
14
|
readonly schemaName: string;
|
|
15
|
+
readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<TenantLifecycle>;
|
|
15
16
|
fromPlainData(obj: any): TenantLifecycle;
|
|
16
17
|
fromDatabase(obj: any): TenantLifecycle | undefined;
|
|
17
18
|
toDatabase(obj: any): Record<string, any>;
|
|
@@ -12,6 +12,7 @@ declare const RoleModel_base: {
|
|
|
12
12
|
delete(id: string): Promise<number>;
|
|
13
13
|
};
|
|
14
14
|
readonly schemaName: string;
|
|
15
|
+
readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<Role>;
|
|
15
16
|
fromPlainData(obj: any): Role;
|
|
16
17
|
fromDatabase(obj: any): Role | undefined;
|
|
17
18
|
toDatabase(obj: any): Record<string, any>;
|
|
@@ -12,6 +12,7 @@ declare const TenantMemberModel_base: {
|
|
|
12
12
|
delete(id: string): Promise<number>;
|
|
13
13
|
};
|
|
14
14
|
readonly schemaName: string;
|
|
15
|
+
readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<TenantMember>;
|
|
15
16
|
fromPlainData(obj: any): TenantMember;
|
|
16
17
|
fromDatabase(obj: any): TenantMember | undefined;
|
|
17
18
|
toDatabase(obj: any): Record<string, any>;
|
|
@@ -12,6 +12,7 @@ declare const TenantModel_base: {
|
|
|
12
12
|
delete(id: string): Promise<number>;
|
|
13
13
|
};
|
|
14
14
|
readonly schemaName: string;
|
|
15
|
+
readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<Tenant>;
|
|
15
16
|
fromPlainData(obj: any): Tenant;
|
|
16
17
|
fromDatabase(obj: any): Tenant | undefined;
|
|
17
18
|
toDatabase(obj: any): Record<string, any>;
|
|
@@ -16,6 +16,7 @@ declare const UserInviteModel_base: {
|
|
|
16
16
|
delete(id: string): Promise<number>;
|
|
17
17
|
};
|
|
18
18
|
readonly schemaName: string;
|
|
19
|
+
readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<UserInvite>;
|
|
19
20
|
fromPlainData(obj: any): UserInvite;
|
|
20
21
|
fromDatabase(obj: any): UserInvite | undefined;
|
|
21
22
|
toDatabase(obj: any): Record<string, any>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Introduction"
|
|
3
|
+
description: "The core contract of the DMS: pages and modules, components, permissions, tenancy, invites, hooks, realtime, quick actions and layout banners."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Introduction
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The root export of `@antelopejs/interface-dms` and the subpaths listed below are the core contract of the AntelopeJS DMS. It encodes one inversion: the backend declares the screen, as serializable data. A page is a controller class, its components are static fields holding builders, and the page's layout route returns the description a shared frontend renders. Adding an admin screen is a backend change.
|
|
11
|
+
|
|
12
|
+
Around that core the interface carries what an admin backend needs to be more than a renderer: hierarchical permissions derived from the page tree, multi-tenant data access scoped by the model handle, lifecycle hooks, realtime topics, and command-palette entries.
|
|
13
|
+
|
|
14
|
+
Everything here is imported from a subpath of `@antelopejs/interface-dms`. Two other domains of the same package complete the picture and are referenced throughout these pages: [Auth](/docs/interfaces/auth/introduction) (`/auth`) for authentication and the `User` table, and [Base](/docs/interfaces/base/introduction) (`/base`) for the concrete component vocabulary (`Form`, `TableView`, `Grid`, the chart family) built on the component types documented here.
|
|
15
|
+
|
|
16
|
+
## Key Features
|
|
17
|
+
|
|
18
|
+
- **Pages as controllers** — `PageController` builds a controller class bound to a URL; `@RegisterPage()` records it, registers its permissions, and serves its layout.
|
|
19
|
+
- **Components that serialize** — a `ComponentBuilder` produces data, not a live component, so one frontend renders every module's screens.
|
|
20
|
+
- **Categories and modules** — a sidebar tree of nested categories, plus modules that own a whole branch under `/modules`.
|
|
21
|
+
- **Dynamic navigation** — providers that resolve extra menu entries per request, so one page can back an entry per project or workspace.
|
|
22
|
+
- **Page extensions** — inject components into a page owned by another module, permissioned in that page's own tree.
|
|
23
|
+
- **Hierarchical permissions** — ids derived from page, component and action, with a base set from roles and an effective set shaped by registered resolvers.
|
|
24
|
+
- **Route guards** — permission, tenant-member and tenant-owner decorators usable as parameter or property decorators.
|
|
25
|
+
- **Tenancy in the handle** — per-tenant schemas and model handles bound to one tenant, rather than a tenant column filtered on every query.
|
|
26
|
+
- **Tenant access gates** — one binary check that denies a tenant at every authorization surface at once.
|
|
27
|
+
- **Invites** — one call that either grants membership to an existing user or issues a token-bearing invite.
|
|
28
|
+
- **Lifecycle hooks** — typed events for tenants, members, invites and registrations, dispatched either to notify or to collect contributions.
|
|
29
|
+
- **Realtime topics** — bind a topic to a page, publish to whoever is on it, and subscribe from the backend to what others publish.
|
|
30
|
+
- **Quick actions** — contribute grouped, permission-gated entries to the command palette.
|
|
31
|
+
|
|
32
|
+
## Dependencies
|
|
33
|
+
|
|
34
|
+
This interface relies on the following AntelopeJS packages:
|
|
35
|
+
|
|
36
|
+
- [@antelopejs/interface-api](https://github.com/AntelopeJS/interface-api) - Controllers, route registration and request context.
|
|
37
|
+
- [@antelopejs/interface-auth](https://github.com/AntelopeJS/interface-auth) - The authentication decorator machinery the guards build on.
|
|
38
|
+
- [@antelopejs/interface-core](https://github.com/AntelopeJS/interface-core) - Interface functions, registering proxies and decorator helpers.
|
|
39
|
+
- [@antelopejs/interface-database-decorators](https://github.com/AntelopeJS/interface-database-decorators) - Table definitions, data models and model injection.
|
|
40
|
+
- [@antelopejs/interface-data-api](https://github.com/AntelopeJS/interface-data-api) - Used by the `/data-controllers` subpath for the built-in members and roles controllers.
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
Declare the application's table and model first. `BookModel` is not a DMS export; this example defines it with the stock flag used by the KPI query.
|
|
45
|
+
|
|
46
|
+
```typescript [db.ts]
|
|
47
|
+
import {
|
|
48
|
+
BasicDataModel,
|
|
49
|
+
Field,
|
|
50
|
+
RegisterTable,
|
|
51
|
+
Table,
|
|
52
|
+
} from "@antelopejs/interface-database-decorators";
|
|
53
|
+
|
|
54
|
+
export const CATALOG_SCHEMA = "example_catalog";
|
|
55
|
+
|
|
56
|
+
@RegisterTable("books", CATALOG_SCHEMA)
|
|
57
|
+
export class Book extends Table {
|
|
58
|
+
@Field("string") declare _id: string;
|
|
59
|
+
@Field("string") declare title: string;
|
|
60
|
+
@Field("boolean") declare inStock: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class BookModel extends BasicDataModel(Book, "books") {}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then declare a category, a page with a component, and the route that component fetches from:
|
|
67
|
+
|
|
68
|
+
```typescript [books-page.ts]
|
|
69
|
+
import { Get } from "@antelopejs/interface-api";
|
|
70
|
+
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
71
|
+
import {
|
|
72
|
+
Category,
|
|
73
|
+
PageController,
|
|
74
|
+
RegisterPage,
|
|
75
|
+
pagesCategory,
|
|
76
|
+
} from "@antelopejs/interface-dms/page";
|
|
77
|
+
import { KpiCard } from "@antelopejs/interface-dms/base";
|
|
78
|
+
import { BookModel } from "./db";
|
|
79
|
+
|
|
80
|
+
export const catalog = Category("catalog", {
|
|
81
|
+
displayName: "Catalog",
|
|
82
|
+
icon: "i-ph-books",
|
|
83
|
+
category: pagesCategory,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
@RegisterPage()
|
|
87
|
+
export class BooksPage extends PageController("books", {
|
|
88
|
+
displayName: "Books",
|
|
89
|
+
description: "Everything currently in the catalog",
|
|
90
|
+
icon: "i-ph-book-open",
|
|
91
|
+
category: catalog,
|
|
92
|
+
order: 1,
|
|
93
|
+
}) {
|
|
94
|
+
static inStockKpi = KpiCard({
|
|
95
|
+
title: "In stock",
|
|
96
|
+
icon: "i-ph-package",
|
|
97
|
+
fetchUrl: "/catalog/books/stats/in-stock",
|
|
98
|
+
valueFormat: "number",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
@Get("/stats/in-stock")
|
|
102
|
+
async inStock() {
|
|
103
|
+
const value = await GetModel(BookModel).table
|
|
104
|
+
.filter((book) => book.key("inStock").eq(true)).count().run();
|
|
105
|
+
return { value };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Import the page and register the schema from your module's entry file, merging this into an existing `start()` if necessary:
|
|
111
|
+
|
|
112
|
+
```typescript [index.ts]
|
|
113
|
+
import "./books-page";
|
|
114
|
+
import { RegisterSchema } from "@antelopejs/interface-database-decorators";
|
|
115
|
+
import { CATALOG_SCHEMA } from "./db";
|
|
116
|
+
|
|
117
|
+
export async function start(): Promise<void> {
|
|
118
|
+
await RegisterSchema(CATALOG_SCHEMA);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The page resolves at `/catalog/books` and its layout at `/dms/pages/catalog/books/pagelayout`: a page controller's routes live under `/dms/pages` (`pageRoutePath`), a prefix the frontend server proxies to the backend, since the page's own path is the page. Its permission id is the chain of ids down from the root category — `pages.catalog.books` — with `pages.catalog.books.inStockKpi` registered underneath it.
|
|
123
|
+
|
|
124
|
+
## Subpath Map
|
|
125
|
+
|
|
126
|
+
| Subpath | Contains | Documented in |
|
|
127
|
+
| ------- | -------- | ------------- |
|
|
128
|
+
| `/page` | Pages, categories, modules, layouts, page extensions, dynamic menus, frontend modules | [Pages and Modules](/docs/interfaces/core/pages-and-modules) |
|
|
129
|
+
| `/component` | `Component`, `ComponentBuilder`, actions, watches | [Components](/docs/interfaces/core/components) |
|
|
130
|
+
| `/component-slots` | `RegisterComponentSlot`, `ComponentSlotResolver`, `SlotOptions` | [Components](/docs/interfaces/core/components#registercomponentslot) |
|
|
131
|
+
| `/attachments` | `SaveComponentFiles` and its request, field and result types | [Pages and Modules](/docs/interfaces/core/pages-and-modules#savecomponentfiles) |
|
|
132
|
+
| `/permission-gate` | Shared `PermissionGate` fields | [Base types](/docs/interfaces/base/types#permissiongate) |
|
|
133
|
+
| `/permissions` | The permission registry and the permission computation | [Permissions](/docs/interfaces/core/permissions) |
|
|
134
|
+
| `/permissions-resolver` | Registering and running permission resolvers | [Permissions](/docs/interfaces/core/permissions) |
|
|
135
|
+
| `/guards` | `AuthUserWithPermission`, `AuthTenantOwner`, `AuthTenantMember` | [Permissions](/docs/interfaces/core/permissions) |
|
|
136
|
+
| `/constants` | Schema names and the default tenant | [Tenancy](/docs/interfaces/core/tenancy) |
|
|
137
|
+
| `/db` | The `Tenant`, `TenantMember`, `Role` and `UserInvite` tables and models | [Tenancy](/docs/interfaces/core/tenancy), [Invites](/docs/interfaces/core/invites) |
|
|
138
|
+
| `/request-tenant` | `getRequestTenantId` | [Tenancy](/docs/interfaces/core/tenancy) |
|
|
139
|
+
| `/tenant-scoped-model` | `TenantScopedModel` | [Tenancy](/docs/interfaces/core/tenancy) |
|
|
140
|
+
| `/tenant-access` | Tenant access gates | [Tenancy](/docs/interfaces/core/tenancy) |
|
|
141
|
+
| `/tenant-ownership` | Membership and ownership writes | [Tenancy](/docs/interfaces/core/tenancy) |
|
|
142
|
+
| `/tenant-lifecycle` | Producer admission and closure before tenant deletion | [Tenancy](/docs/interfaces/core/tenancy#tenant-lifecycle-admission) |
|
|
143
|
+
| `/data-controllers` | The built-in members and roles data controllers | [Tenancy](/docs/interfaces/core/tenancy) |
|
|
144
|
+
| `/invites` | Inviting a user into a tenant | [Invites](/docs/interfaces/core/invites) |
|
|
145
|
+
| `/invite-extensions` | Invite form contributions, callbacks and availability | [Invites](/docs/interfaces/core/invites#registerinviteextension) |
|
|
146
|
+
| `/invite-resolution` | Durable terminal decisions and retryable effects | [Invites](/docs/interfaces/core/invites#durable-invite-resolution) |
|
|
147
|
+
| `/hooks` | The `Hook` enum, handlers and dispatch | [Hooks](/docs/interfaces/core/hooks) |
|
|
148
|
+
| `/tenant-export` | The tenant data export archive, contribution and manifest types, re-exported from `/hooks` | [Hooks](/docs/interfaces/core/hooks) |
|
|
149
|
+
| `/realtime` | Page topics, publishing and backend subscription | [Realtime](/docs/interfaces/core/realtime) |
|
|
150
|
+
| `/quick-actions` | Command-palette entries | [Quick Actions](/docs/interfaces/core/quick-actions) |
|
|
151
|
+
| `/layout-banners` | Global banners under the dashboard header | [Layout Banners](/docs/interfaces/core/layout-banners) |
|
|
152
|
+
| `/uploads` | Signed upload tokens for the file and image fields of a serialized component | [Pages and Modules](/docs/interfaces/core/pages-and-modules) |
|
|
153
|
+
| `/types` | `MaybePromise`, the one shared type alias | [Pages and Modules](/docs/interfaces/core/pages-and-modules) |
|
|
154
|
+
| `/api-base-url`, `/client-base-url` | `GetApiBaseUrl`, `GetClientBaseUrl`: the public origins of the API and the dashboard | [Pages and Modules](/docs/interfaces/core/pages-and-modules#public-urls) |
|
|
155
|
+
| `/client-ip` | `getClientIp`, `ResolveClientIp`: a request's client address behind the trusted proxies | [Pages and Modules](/docs/interfaces/core/pages-and-modules#client-address) |
|
|
156
|
+
|
|
157
|
+
Paths under `internal/`, symbols tagged `@internal`, and `internal` namespaces are implementation wiring, not supported consumer APIs. The wildcard export does not change that boundary.
|
|
158
|
+
|
|
159
|
+
## Documentation Sections
|
|
160
|
+
|
|
161
|
+
- [Pages and Modules](/docs/interfaces/core/pages-and-modules) - Declare pages, group them into categories, own a branch of the sidebar with a module, and extend a page from another one
|
|
162
|
+
- [Components](/docs/interfaces/core/components) - Build serializable components, declare actions and watches
|
|
163
|
+
- [Permissions](/docs/interfaces/core/permissions) - Register permissions, compute a user's effective set, guard routes
|
|
164
|
+
- [Tenancy](/docs/interfaces/core/tenancy) - Schemas, the tenancy tables, tenant-scoped models and access gates
|
|
165
|
+
- [Invites](/docs/interfaces/core/invites) - Bring a user into a tenant by email
|
|
166
|
+
- [Hooks](/docs/interfaces/core/hooks) - React to platform events and contribute to the tenant data export
|
|
167
|
+
- [Realtime](/docs/interfaces/core/realtime) - Bind topics to pages, publish messages and subscribe to them
|
|
168
|
+
- [Quick Actions](/docs/interfaces/core/quick-actions) - Contribute entries to the command palette
|
|
169
|
+
- [Layout Banners](/docs/interfaces/core/layout-banners) - Show a global banner under the dashboard header, decided per request
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Layout Banners"
|
|
3
|
+
description: "A layout banner is a global message the backend adds under the dashboard header: a colored strip carrying a line of text and its link buttons, or a component of your frontend layer."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Layout Banners
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
A layout banner is a global message the backend adds under the dashboard header: a colored strip carrying a line of text and its link buttons, or a component of your frontend layer. It renders on every page of the dashboard layout — tenant pages and the module back-office pages under `/modules` alike — and banners stack by `order`.
|
|
11
|
+
|
|
12
|
+
Whether a banner shows is decided per request, on the server, while the site layout is built. The answer is part of the server-rendered page, so a banner never flashes in or out after hydration.
|
|
13
|
+
|
|
14
|
+
Everything lives in `@antelopejs/interface-dms/layout-banners`.
|
|
15
|
+
|
|
16
|
+
## Register a Banner
|
|
17
|
+
|
|
18
|
+
`RegisterLayoutBanner(info)` registers the banner and returns a function removing it. Register at import time, next to the state the banner reports on; the registration is dropped automatically when your module stops.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import {
|
|
22
|
+
LayoutBannerVariant,
|
|
23
|
+
RegisterLayoutBanner,
|
|
24
|
+
} from "@antelopejs/interface-dms/layout-banners";
|
|
25
|
+
import { isMaintenanceScheduled } from "./maintenance";
|
|
26
|
+
|
|
27
|
+
RegisterLayoutBanner({
|
|
28
|
+
key: "my-app:maintenance",
|
|
29
|
+
variant: LayoutBannerVariant.WARNING,
|
|
30
|
+
text: "$my_app.banners.maintenance",
|
|
31
|
+
order: 10,
|
|
32
|
+
dismissible: true,
|
|
33
|
+
visible: async ({ tenantId }) => isMaintenanceScheduled(tenantId),
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The text can be a [composed text](/docs/components/block-fundamentals#composed-texts) — an i18n key and the raw values it names, written in the reader's language — and `actions` adds link buttons after it, the last one leading:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
RegisterLayoutBanner({
|
|
41
|
+
key: "my-app:maintenance",
|
|
42
|
+
variant: LayoutBannerVariant.WARNING,
|
|
43
|
+
text: {
|
|
44
|
+
key: "my_app.banners.maintenance_at", // "Maintenance {date}"
|
|
45
|
+
params: { date: { type: "datetime", value: "2026-10-12T22:00:00Z" } },
|
|
46
|
+
},
|
|
47
|
+
actions: [{ label: "$my_app.banners.status", to: "https://status.example.com" }],
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Resolve the Content per Request
|
|
52
|
+
|
|
53
|
+
When the message names the state that makes the banner show — the invoice that failed, its amount, the day the workspace is suspended — `resolve(context)` builds the text and the buttons for each request, or answers `undefined` to hide the banner. It replaces `visible`: one read decides both.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
RegisterLayoutBanner({
|
|
57
|
+
key: "saas:past-due",
|
|
58
|
+
variant: LayoutBannerVariant.ERROR,
|
|
59
|
+
resolve: async ({ tenantId }) => {
|
|
60
|
+
const invoice = await openInvoice(tenantId);
|
|
61
|
+
if (!invoice) return undefined;
|
|
62
|
+
return {
|
|
63
|
+
text: {
|
|
64
|
+
key: "saas.banner.past_due", // "Invoice {invoice} of {amount} failed, retry {date}"
|
|
65
|
+
params: {
|
|
66
|
+
invoice: invoice.number,
|
|
67
|
+
amount: { type: "money", value: invoice.amountDue, currency: invoice.currency },
|
|
68
|
+
date: { type: "date", value: invoice.nextAttemptAt, format: "day" },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
actions: [{ label: "$saas.banner.pay", to: "/settings/workspace/billing?pay=1" }],
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The server sends the raw values; the browser formats them for the reader's language, time zone and clock, so the resolver never formats text. A banner like this one needs no frontend component.
|
|
78
|
+
|
|
79
|
+
## Mount a Component
|
|
80
|
+
|
|
81
|
+
A banner can mount a global component of your [frontend layer](/docs/building/frontend-layer) instead of a text. The DMS still draws the chrome around it — color, icon, close button and ARIA role — and passes `props` to the component:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
RegisterLayoutBanner({
|
|
85
|
+
key: "my-app:trial",
|
|
86
|
+
variant: LayoutBannerVariant.INFO,
|
|
87
|
+
component: "MyAppTrialBanner",
|
|
88
|
+
props: { upgradeUrl: "/settings/billing" },
|
|
89
|
+
visible: ({ isOwner }) => isOwner,
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Re-registering a `key` replaces the banner.
|
|
94
|
+
|
|
95
|
+
## `LayoutBannerInfo`
|
|
96
|
+
|
|
97
|
+
| Field | Type | Description |
|
|
98
|
+
| ----- | ---- | ----------- |
|
|
99
|
+
| `key` | `string` | Unique across the deployment. Required. Also what a dismissal remembers: give a banner a new key when it must show again to users who dismissed it. |
|
|
100
|
+
| `variant` | `LayoutBannerVariant` | `"info"`, `"warning"` or `"error"`. Required. Picks the color, the default icon and the ARIA role. |
|
|
101
|
+
| `text` | `string \| ComposedText` | The message. A `$`-prefixed value resolves as an i18n key; a `ComposedText` is written in the reader's language. Exclusive with `component` and `resolve`; one of the three is required. |
|
|
102
|
+
| `actions` | `BlockLinkAction[]` | Link buttons after `text`, the last one leading. Labels follow the `$` convention. |
|
|
103
|
+
| `component` | `string` | Name of a global component rendered as the banner's content. Exclusive with `text` and `resolve`. |
|
|
104
|
+
| `props` | `Record<string, unknown>` | Props passed to `component`. Must be serializable. |
|
|
105
|
+
| `order` | `number` | Stack order, ascending from the top. Defaults to `0`; ties keep the registration order. |
|
|
106
|
+
| `icon` | `string` | Leading icon. Defaults to one matching the variant. |
|
|
107
|
+
| `dismissible` | `boolean` | Whether the user can close the banner. Defaults to `false`. |
|
|
108
|
+
| `visible` | `LayoutBannerVisibility` | Decides per request whether a `text` or `component` banner shows. Absent means always. |
|
|
109
|
+
| `resolve` | `LayoutBannerResolver` | Builds `{ text, actions? }` per request, or `undefined` to hide the banner. Exclusive with `text`, `component` and `visible`. |
|
|
110
|
+
|
|
111
|
+
## Visibility
|
|
112
|
+
|
|
113
|
+
`visible(context)` returns a boolean, or a promise of one; `resolve(context)` returns a `LayoutBannerContent` (`{ text, actions? }`), `undefined`, or a promise of either. They run server-side for every site layout request, all resolvers together, so the request waits for the slowest one rather than for their sum. A resolver that throws or does not answer within two seconds hides its banner for that request, as does a `visible` returning anything but `true` or a `resolve` answering no text; the other banners are unaffected.
|
|
114
|
+
|
|
115
|
+
### `LayoutBannerContext`
|
|
116
|
+
|
|
117
|
+
| Field | Type | Description |
|
|
118
|
+
| ----- | ---- | ----------- |
|
|
119
|
+
| `user` | `User \| undefined` | The caller; `undefined` for an unauthenticated request. |
|
|
120
|
+
| `tenantId` | `string` | The tenant the site layout is built for. |
|
|
121
|
+
| `permissions` | `Set<string>` | The caller's effective permissions in the tenant. |
|
|
122
|
+
| `isOwner` | `boolean` | Whether the caller holds the wildcard permission. |
|
|
123
|
+
| `isTenantAccessDenied` | `boolean` | Whether a [tenant access gate](/docs/interfaces/core/tenancy) currently refuses the caller. |
|
|
124
|
+
|
|
125
|
+
Unlike product surfaces, a banner reads the permissions a denying access gate would hide: telling a suspended or past-due workspace why is exactly what a banner is for. Check `isTenantAccessDenied` when the two situations call for different banners.
|
|
126
|
+
|
|
127
|
+
The resolvers run whenever a client fetches its site layout — on a full page load and after a menu invalidation. When the state a resolver reads changes, call `NotifyMenuChanged(tenantId)` from `@antelopejs/interface-dms/page` so the connected clients of that tenant re-fetch it and the banner appears or goes without a reload.
|
|
128
|
+
|
|
129
|
+
## Rendering
|
|
130
|
+
|
|
131
|
+
Banners render under the header of the dashboard layout, full width, in `order`, a text banner's `actions` on its right. Each one is its own live region: `error` banners carry `role="alert"`, `info` and `warning` banners `role="status"`.
|
|
132
|
+
|
|
133
|
+
A dismissible banner shows a close button. The dismissal lasts for the browser session and is kept in the `dms-dismissed-banners` cookie, which the server reads too, so a dismissed banner is already absent from the server-rendered page. A banner that is not dismissible ignores any dismissal recorded under its key.
|