@antelopejs/interface-dms 0.7.1 → 0.7.3
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 +86 -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-registry/index.d.ts +1 -0
- package/dist/base/block-registry/index.js +1 -0
- package/dist/base/block-registry/index.js.map +1 -1
- package/dist/base/block-registry/presentation.d.ts +21 -0
- package/dist/base/block-registry/presentation.js +108 -0
- package/dist/base/block-registry/presentation.js.map +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/live-toggle.d.ts +2 -2
- package/dist/base/period-selector.d.ts +2 -2
- package/dist/base/placeholder.js +2 -2
- package/dist/base/placeholder.js.map +1 -1
- 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 +649 -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 +358 -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 +437 -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 +81 -0
- package/docs/4.notifications/2.categories-and-subjects.md +214 -0
- package/docs/4.notifications/3.sending.md +285 -0
- package/docs/5.html-render/.navigation.yml +2 -0
- package/docs/5.html-render/1.introduction.md +386 -0
- package/docs/5.html-render/2.built-in-emails.md +157 -0
- package/docs/images/emails-admin-invite.png +0 -0
- package/docs/images/emails-change-notice.png +0 -0
- package/docs/images/emails-change-verification.png +0 -0
- package/docs/images/emails-digest-logo-dark.png +0 -0
- package/docs/images/emails-digest-logo-light.png +0 -0
- package/docs/images/emails-digest.png +0 -0
- package/docs/images/emails-export-partial.png +0 -0
- package/docs/images/emails-export-ready.png +0 -0
- package/docs/images/emails-reset-password.png +0 -0
- package/docs/images/emails-two-factor.png +0 -0
- package/docs/images/emails-user-validation.png +0 -0
- package/docs/index.md +109 -0
- package/docs/tsconfig.json +11 -0
- package/package.json +25 -18
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Introduction"
|
|
3
|
+
description: "Turn a named template and a props object into an HTML string, rendered by the frontend and delivered by your code — as an email, a file or a response."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Introduction
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The `html-render` domain of `@antelopejs/interface-dms` turns a named template plus a props object into an HTML string. You declare a template once with `RegisterHtmlTemplate`, which records the name in a process-wide registry and hands back a typed reference; passing that reference to `GenerateHtml` produces the rendered HTML.
|
|
11
|
+
|
|
12
|
+
Rendering itself does not happen in the backend process. `GenerateHtml` performs an HTTP `POST` to the render endpoint configured on the DMS (`htmlRender.renderEndpoint`), authenticated with a signed service token, and returns the HTML the endpoint sends back. The backend therefore owns the template *names* and the props *types*; the markup lives with the renderer behind that endpoint.
|
|
13
|
+
|
|
14
|
+
The interface produces HTML and nothing else. Delivering that HTML — as a transactional email, as a file, as a response body — is your code's job.
|
|
15
|
+
|
|
16
|
+
Everything below is exported from a single subpath:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import {
|
|
20
|
+
GenerateHtml,
|
|
21
|
+
GetAllHtmlTemplates,
|
|
22
|
+
GetHtmlTemplate,
|
|
23
|
+
RegisterHtmlTemplate,
|
|
24
|
+
} from "@antelopejs/interface-dms/html-render";
|
|
25
|
+
import type {
|
|
26
|
+
HtmlRenderConfig,
|
|
27
|
+
HtmlTemplateInfo,
|
|
28
|
+
HtmlTemplateRef,
|
|
29
|
+
} from "@antelopejs/interface-dms/html-render";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Key Features
|
|
33
|
+
|
|
34
|
+
- **Named template registry** — `RegisterHtmlTemplate` records a template name at import time, so the whole process shares one catalogue of renderable templates.
|
|
35
|
+
- **Documented props** — the type parameter on `RegisterHtmlTemplate` travels with the returned `HtmlTemplateRef`, recording on the reference itself which props the template expects.
|
|
36
|
+
- **Remote rendering over HTTP** — `GenerateHtml` calls the configured render endpoint with a signed service token and resolves to the returned HTML string.
|
|
37
|
+
- **Registry inspection** — `GetHtmlTemplate` and `GetAllHtmlTemplates` read back what has been registered, one name at a time or in bulk.
|
|
38
|
+
- **Configurable endpoint and token** — `HtmlRenderConfig` types the `htmlRender` block of the DMS module configuration: endpoint URL, signing secret, token lifetime.
|
|
39
|
+
|
|
40
|
+
## Dependencies
|
|
41
|
+
|
|
42
|
+
This interface relies on the following AntelopeJS packages:
|
|
43
|
+
|
|
44
|
+
- [@antelopejs/interface-core](https://github.com/AntelopeJS/interface-core) - `InterfaceFunction` and `RegisteringProxy`, the primitives the interface is built from.
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
Register a template next to the code that uses it, then render it with props.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import {
|
|
52
|
+
GenerateHtml,
|
|
53
|
+
RegisterHtmlTemplate,
|
|
54
|
+
} from "@antelopejs/interface-dms/html-render";
|
|
55
|
+
|
|
56
|
+
interface WelcomeProps {
|
|
57
|
+
userName: string;
|
|
58
|
+
activationLink: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Registration happens at import time; keep the ref in module scope.
|
|
62
|
+
const WelcomeTemplate = RegisterHtmlTemplate<WelcomeProps>("EmailWelcome");
|
|
63
|
+
|
|
64
|
+
export async function buildWelcomeHtml(
|
|
65
|
+
userName: string,
|
|
66
|
+
activationLink: string,
|
|
67
|
+
): Promise<string> {
|
|
68
|
+
return GenerateHtml(WelcomeTemplate, { userName, activationLink });
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`GenerateHtml` needs the render endpoint to be reachable, so it belongs in request or job code — do not call it at import time.
|
|
73
|
+
|
|
74
|
+
## RegisterHtmlTemplate
|
|
75
|
+
|
|
76
|
+
Registers a template name and returns the reference you render with.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
function RegisterHtmlTemplate<TProps = unknown>(
|
|
80
|
+
name: string,
|
|
81
|
+
): HtmlTemplateRef<TProps>;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Parameter | Description |
|
|
85
|
+
| --------- | ----------- |
|
|
86
|
+
| `name` | The template identifier. It is the key the registry stores the template under, and the `templateName` sent to the render endpoint. |
|
|
87
|
+
|
|
88
|
+
The type parameter `TProps` declares the props the template expects. It is carried by the returned [`HtmlTemplateRef`](#htmltemplateref) as a record of the template's shape; [`GenerateHtml`](#generatehtml) takes its `props` argument as `unknown`, so the compiler does not check the two against each other at the call site. Omitting `TProps` leaves the reference's props type as `unknown`.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { RegisterHtmlTemplate } from "@antelopejs/interface-dms/html-render";
|
|
92
|
+
|
|
93
|
+
interface InvoiceProps {
|
|
94
|
+
invoiceNumber: string;
|
|
95
|
+
amount: string;
|
|
96
|
+
dueDate: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const InvoiceTemplate =
|
|
100
|
+
RegisterHtmlTemplate<InvoiceProps>("EmailInvoice");
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The registry is keyed by name, so registering an existing name replaces the earlier entry. The DMS registers seven names for its own emails, listed in [Built-in Emails](/docs/interfaces/html-render/built-in-emails) — pick different names for your templates.
|
|
104
|
+
|
|
105
|
+
Call `RegisterHtmlTemplate` at module scope, in a file your module imports during startup. The returned reference is a plain object, so exporting it from that module is the usual way to share it.
|
|
106
|
+
|
|
107
|
+
The subpath also exports an `internal` namespace holding the registering proxy this function writes through. It is implementation detail — register through `RegisterHtmlTemplate`.
|
|
108
|
+
|
|
109
|
+
## GenerateHtml
|
|
110
|
+
|
|
111
|
+
Renders a registered template and resolves to the resulting HTML.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const GenerateHtml: (
|
|
115
|
+
template: HtmlTemplateRef<unknown>,
|
|
116
|
+
props: unknown,
|
|
117
|
+
) => Promise<string>;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Parameter | Description |
|
|
121
|
+
| --------- | ----------- |
|
|
122
|
+
| `template` | The reference returned by `RegisterHtmlTemplate`. Pass the reference, not the name string. |
|
|
123
|
+
| `props` | The data the template renders with, serialised into the request body. The render endpoint expects a JSON object and rejects anything else with a `400`, so keep the props type an object shape. Typed `unknown` at this boundary, so the props type recorded on the reference is not enforced here — match it yourself. |
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { GenerateHtml } from "@antelopejs/interface-dms/html-render";
|
|
127
|
+
import { InvoiceTemplate } from "./templates";
|
|
128
|
+
|
|
129
|
+
export async function renderInvoice(invoiceNumber: string): Promise<string> {
|
|
130
|
+
return GenerateHtml(InvoiceTemplate, {
|
|
131
|
+
invoiceNumber,
|
|
132
|
+
amount: "240.00 EUR",
|
|
133
|
+
dueDate: "2026-08-15",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The call `POST`s `{ templateName, props }` as JSON to `htmlRender.renderEndpoint`, with a service token signed from `htmlRender.serviceSecret` in the `x-dms-service-token` header, and reads the `html` field out of the JSON response.
|
|
139
|
+
|
|
140
|
+
`GenerateHtml` does not consult the registry before calling out: it sends whatever name the reference carries. A name the render endpoint does not know surfaces as a rejected promise, not as a missing-template check on the backend.
|
|
141
|
+
|
|
142
|
+
On a non-2xx response the promise rejects with an `Error` whose message starts with `Failed to render HTML template:`, followed by the response body's `error` string, its `statusMessage`, or `HTTP <status>`. When the endpoint is unreachable the underlying `fetch` failure propagates instead, without that prefix. Wrap the call when a failed render must not abort the surrounding operation:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { Logging } from "@antelopejs/interface-core/logging";
|
|
146
|
+
import { GenerateHtml } from "@antelopejs/interface-dms/html-render";
|
|
147
|
+
import { InvoiceTemplate } from "./templates";
|
|
148
|
+
|
|
149
|
+
export async function renderInvoiceOrNull(
|
|
150
|
+
invoiceNumber: string,
|
|
151
|
+
): Promise<string | null> {
|
|
152
|
+
try {
|
|
153
|
+
return await GenerateHtml(InvoiceTemplate, {
|
|
154
|
+
invoiceNumber,
|
|
155
|
+
amount: "240.00 EUR",
|
|
156
|
+
dueDate: "2026-08-15",
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
Logging.Error(`Invoice render failed: ${(error as Error).message}`);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## GetHtmlTemplate
|
|
166
|
+
|
|
167
|
+
Looks a single template up in the registry.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const GetHtmlTemplate: (key: string) => Promise<HtmlTemplateInfo | undefined>;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
| Parameter | Description |
|
|
174
|
+
| --------- | ----------- |
|
|
175
|
+
| `key` | The name the template was registered under. |
|
|
176
|
+
|
|
177
|
+
Resolves to the [`HtmlTemplateInfo`](#htmltemplateinfo) stored for that name, or to `undefined` when nothing is registered under it.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { GetHtmlTemplate } from "@antelopejs/interface-dms/html-render";
|
|
181
|
+
|
|
182
|
+
export async function isTemplateRegistered(name: string): Promise<boolean> {
|
|
183
|
+
return (await GetHtmlTemplate(name)) !== undefined;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## GetAllHtmlTemplates
|
|
188
|
+
|
|
189
|
+
Resolves to every registered template, keyed by name.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const GetAllHtmlTemplates: () => Promise<Record<string, HtmlTemplateInfo>>;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { GetAllHtmlTemplates } from "@antelopejs/interface-dms/html-render";
|
|
197
|
+
|
|
198
|
+
export async function listTemplateNames(): Promise<string[]> {
|
|
199
|
+
return Object.keys(await GetAllHtmlTemplates());
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The result is a fresh object built from the registry, so mutating it does not change what is registered. Because registration happens as modules are imported, call this after startup rather than at import time if you want the complete set.
|
|
204
|
+
|
|
205
|
+
## HtmlTemplateInfo
|
|
206
|
+
|
|
207
|
+
What the registry stores for each template, and what `GetHtmlTemplate` and `GetAllHtmlTemplates` resolve to.
|
|
208
|
+
|
|
209
|
+
| Field | Type | Description |
|
|
210
|
+
| ----- | ---- | ----------- |
|
|
211
|
+
| `name` | `string` | The registered template name. |
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import type { HtmlTemplateInfo } from "@antelopejs/interface-dms/html-render";
|
|
215
|
+
import { GetAllHtmlTemplates } from "@antelopejs/interface-dms/html-render";
|
|
216
|
+
|
|
217
|
+
export async function describeTemplates(): Promise<string> {
|
|
218
|
+
const templates: Record<string, HtmlTemplateInfo> =
|
|
219
|
+
await GetAllHtmlTemplates();
|
|
220
|
+
|
|
221
|
+
return Object.values(templates)
|
|
222
|
+
.map((info) => info.name)
|
|
223
|
+
.join(", ");
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## HtmlTemplateRef
|
|
228
|
+
|
|
229
|
+
The handle `RegisterHtmlTemplate` returns and `GenerateHtml` accepts.
|
|
230
|
+
|
|
231
|
+
| Field | Type | Description |
|
|
232
|
+
| ----- | ---- | ----------- |
|
|
233
|
+
| `name` | `string` | The template name sent to the render endpoint. |
|
|
234
|
+
| `_propsType` | `TProps \| undefined` | Type-only carrier for the props type. It is never assigned a value; do not read it. |
|
|
235
|
+
|
|
236
|
+
Annotate the reference explicitly when you collect handles in a shared module and want the props type visible in that module's declarations:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import type { HtmlTemplateRef } from "@antelopejs/interface-dms/html-render";
|
|
240
|
+
import { RegisterHtmlTemplate } from "@antelopejs/interface-dms/html-render";
|
|
241
|
+
|
|
242
|
+
interface AlertProps {
|
|
243
|
+
title: string;
|
|
244
|
+
body: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const AlertTemplate: HtmlTemplateRef<AlertProps> =
|
|
248
|
+
RegisterHtmlTemplate<AlertProps>("EmailAlert");
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## HtmlRenderConfig
|
|
252
|
+
|
|
253
|
+
Types the `htmlRender` block of the DMS module configuration in `antelope.config.ts`.
|
|
254
|
+
|
|
255
|
+
| Option | Type | Description |
|
|
256
|
+
| ------ | ---- | ----------- |
|
|
257
|
+
| `renderEndpoint` | `string` | Absolute URL `GenerateHtml` posts to. Default `http://localhost:3001/api/html/render`. |
|
|
258
|
+
| `serviceSecret` | `string` | Secret used to sign the service token that authenticates the render call. Default `"dev"` — set a strong value in production. |
|
|
259
|
+
| `serviceTokenLifetime` | `number` | Lifetime of that service token, in milliseconds. The frontend render server rejects a token valid for more than five minutes. Default `300000` (5 minutes). |
|
|
260
|
+
|
|
261
|
+
The DMS merges the block you supply over those defaults, so a configuration only needs the keys it changes:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { defineConfig } from "@antelopejs/interface-core/config";
|
|
265
|
+
|
|
266
|
+
const renderSecret = process.env.DMS_HTML_RENDER_SECRET;
|
|
267
|
+
if (!renderSecret) {
|
|
268
|
+
throw new Error("DMS_HTML_RENDER_SECRET is not set");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export default defineConfig({
|
|
272
|
+
name: "acme-admin",
|
|
273
|
+
modules: {
|
|
274
|
+
dms: {
|
|
275
|
+
source: {
|
|
276
|
+
type: "package",
|
|
277
|
+
package: "@antelopejs/dms",
|
|
278
|
+
version: ">=0.4.3 <1.0.0",
|
|
279
|
+
},
|
|
280
|
+
config: {
|
|
281
|
+
htmlRender: {
|
|
282
|
+
renderEndpoint: "https://admin.example.com/api/html/render",
|
|
283
|
+
serviceSecret: renderSecret,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The Configuration chapter of the DMS guide covers where this block sits alongside the other DMS options.
|
|
292
|
+
|
|
293
|
+
## Email Layout
|
|
294
|
+
|
|
295
|
+
The DMS ships the frame of its own emails as `EmailLayout`, which a module's email template (registered through `dms.email.ts`) may reuse: the branding logo, a card, an optional heading and a `footer` slot below a separator.
|
|
296
|
+
|
|
297
|
+
```vue
|
|
298
|
+
<script setup lang="ts">
|
|
299
|
+
import EmailLayout from "#dms-layout/app/emails/components/EmailLayout.vue";
|
|
300
|
+
|
|
301
|
+
defineProps<{ subject: string; preheader: string; locale: string }>();
|
|
302
|
+
</script>
|
|
303
|
+
|
|
304
|
+
<template>
|
|
305
|
+
<EmailLayout
|
|
306
|
+
:lang="locale"
|
|
307
|
+
:preheader="preheader"
|
|
308
|
+
:divider="false"
|
|
309
|
+
public-logo-only
|
|
310
|
+
>
|
|
311
|
+
<template #head>
|
|
312
|
+
<title>{{ subject }}</title>
|
|
313
|
+
</template>
|
|
314
|
+
<EText>Your order has shipped.</EText>
|
|
315
|
+
<template #footer>
|
|
316
|
+
<EText>Acme Inc.</EText>
|
|
317
|
+
</template>
|
|
318
|
+
</EmailLayout>
|
|
319
|
+
</template>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
| Prop | Type | Description |
|
|
323
|
+
| ---- | ---- | ----------- |
|
|
324
|
+
| `title` | `string` (optional) | Drawn as the `h1` heading. Without it the email has no heading. |
|
|
325
|
+
| `lang` | `string` (optional) | The `lang` of the document. Default `"en"`. |
|
|
326
|
+
| `preheader` | `string` (optional) | Inbox preview text, hidden in the body. |
|
|
327
|
+
| `divider` | `boolean` (optional) | Draws the separator above the `footer` slot. Default `true`. |
|
|
328
|
+
| `logoAlt` | `string` (optional) | Alternative text of the logo. Default `"Logo"`; pass `""` for a decorative logo. |
|
|
329
|
+
| `publicLogoOnly` | `boolean` (optional) | Leaves the logo out unless a mail client can load it (see below). Default `false`. |
|
|
330
|
+
|
|
331
|
+
The `head` slot adds elements to the document head (a `<title>`, a viewport `<meta>`).
|
|
332
|
+
|
|
333
|
+
The logo is `branding.logo.email.light` from the app config, or `branding.logo.default.light` without an email logo, made absolute against `public.dms.clientBaseUrl` when it starts with `/`. A recipient's mail client fetches it from the internet, and Gmail and Outlook refuse SVG: with `publicLogoOnly`, the layout leaves the logo out when it is an SVG, is not an `http(s)` URL, or is served from a host a mail client cannot reach (`localhost`, `*.local`, `*.internal`, a loopback, private or link-local address), rather than drawing a broken image. Set a PNG logo and a public `clientBaseUrl` for it to show. A `branding.logo.email.dark` goes through the same rules and, when it differs from the light one, is offered to the mail clients in dark mode: the logo is then drawn as `<picture><source media="(prefers-color-scheme: dark)" srcset="…"><img …></picture>`, and a client that ignores the source draws the light image. The same check is exported as `emailLogoUrl(logoPath, clientBaseUrl)` from `#dms-layout/app/emails/components/index`, for a template that draws its own header.
|
|
334
|
+
|
|
335
|
+
### A rendered custom digest
|
|
336
|
+
|
|
337
|
+
The playground's `EmailPlaygroundDigest` reuses this layout with a preheader, `divider="false"` and `public-logo-only`. Here is its actual rendered HTML in English, with fictitious activity for Alex Morgan. With the default SVG logo, the header image is intentionally absent.
|
|
338
|
+
|
|
339
|
+

|
|
340
|
+
|
|
341
|
+
The same template below uses two demonstration PNG logos supplied through the app config. The light-mode capture selects the teal logo; the dark-mode capture selects the navy logo. The reserved demo hostname is mapped to a local image server for these captures, so they do not establish that a recipient can fetch the logo from the internet.
|
|
342
|
+
|
|
343
|
+

|
|
344
|
+
|
|
345
|
+

|
|
346
|
+
|
|
347
|
+
Dark mode changes the selected logo, not the card: `EmailLayout` keeps its white card and pale background in both browser modes. Without a dark logo source, the unbranded examples look identical in both modes. These captures use the real frontend render endpoint and show the returned HTML directly, not the playground's code viewer. No email is sent. Mail clients may strip `<picture>`, block remote images or transform colors; test delivery and appearance in your target clients separately.
|
|
348
|
+
|
|
349
|
+
## Sending Rendered HTML as Email
|
|
350
|
+
|
|
351
|
+
Rendering and sending are separate steps. Render with `GenerateHtml`, then hand the string to the standard AntelopeJS email interface, which needs an implementing module (such as `@antelopejs/nodemailer`) loaded in your project configuration.
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import { Send } from "@antelopejs/interface-email";
|
|
355
|
+
import {
|
|
356
|
+
GenerateHtml,
|
|
357
|
+
RegisterHtmlTemplate,
|
|
358
|
+
} from "@antelopejs/interface-dms/html-render";
|
|
359
|
+
|
|
360
|
+
interface ReceiptProps {
|
|
361
|
+
userName: string;
|
|
362
|
+
orderId: string;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const ReceiptTemplate = RegisterHtmlTemplate<ReceiptProps>("EmailReceipt");
|
|
366
|
+
|
|
367
|
+
export async function sendReceipt(
|
|
368
|
+
email: string,
|
|
369
|
+
userName: string,
|
|
370
|
+
orderId: string,
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
const html = await GenerateHtml(ReceiptTemplate, { userName, orderId });
|
|
373
|
+
|
|
374
|
+
const result = await Send({
|
|
375
|
+
to: email,
|
|
376
|
+
subject: `Your receipt for order ${orderId}`,
|
|
377
|
+
html,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (!result.success) {
|
|
381
|
+
throw new Error(`Failed to send receipt: ${result.error?.message}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
This is the shape the DMS uses for its own account emails, and it keeps the two failure modes apart: a rejected `GenerateHtml` means the HTML was never produced, while a `success: false` response from `Send` means it was produced but not delivered.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Built-in Emails"
|
|
3
|
+
description: "The DMS sends seven emails of its own."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Built-in Emails
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The DMS sends seven emails of its own. Each one is a template registered through [`RegisterHtmlTemplate`](/docs/interfaces/html-render/introduction#registerhtmltemplate), rendered by the frontend server from a Vue component in `frontend-vue/layers/dms-layout/app/emails/`, and delivered through `@antelopejs/interface-email`. They share one frame, `EmailLayout`: the branding logo, a white card, a heading, the body, a separator and a footer.
|
|
11
|
+
|
|
12
|
+
| Template | Sent when | Sent to | English subject |
|
|
13
|
+
| -------- | --------- | ------- | ------- |
|
|
14
|
+
| `EmailUserValidation` | A user signs up while `auth.mustValidateEmail` is on, or asks for a new code | The new account's address | `Verify Your Email` |
|
|
15
|
+
| `EmailResetPassword` | A user asks for a password reset | The account's address | `Reset Your Password` |
|
|
16
|
+
| `EmailTwoFactor` | A user signs in with email as their second factor | The account's address | `Your verification code` |
|
|
17
|
+
| `EmailAdminInvite` | A member is invited to the platform or to a workspace | The invited address | `You're invited to join <workspace> on <platform>`, shorter when a name is unknown |
|
|
18
|
+
| `EmailChangeVerification` | A user asks to change their sign-in email | The new address | `Confirm your new sign-in email` |
|
|
19
|
+
| `EmailChangeNotice` | The same request | The current address | `Your sign-in email is being changed` |
|
|
20
|
+
| `EmailExportReady` | A data export delivered by email has finished | The user who asked for it | `Your data export is ready` |
|
|
21
|
+
|
|
22
|
+
Each email, subject included, is written in the recipient's language, English or French (see [Language](#language)).
|
|
23
|
+
|
|
24
|
+
These names are taken: pick other names for your own emails. To change what a built-in email looks like, see [Replacing a built-in email](#replacing-a-built-in-email).
|
|
25
|
+
|
|
26
|
+
## Props
|
|
27
|
+
|
|
28
|
+
What the backend passes to each template. A template that replaces a built-in one receives the same props.
|
|
29
|
+
|
|
30
|
+
| Template | Props |
|
|
31
|
+
| -------- | ----- |
|
|
32
|
+
| `EmailUserValidation` | `userName`, `validationCode`, `expiresInHours` (from `auth.emailValidationTokenLifetime`), `expiresIn` |
|
|
33
|
+
| `EmailResetPassword` | `userName`, `resetCode`, `expiresInHours` (from `auth.passwordRecoverTokenLifetime`), `expiresIn` |
|
|
34
|
+
| `EmailTwoFactor` | `userName`, `verificationCode` |
|
|
35
|
+
| `EmailAdminInvite` | `email`, `signupLink`, `expiresInDays`, and when known `workspaceName`, `inviterName`, `platformName` (`meta.title`) |
|
|
36
|
+
| `EmailChangeVerification` | `userName`, `newEmail`, `code`, `expiresInHours` (from `auth.emailValidationTokenLifetime`), `expiresIn` |
|
|
37
|
+
| `EmailChangeNotice` | `userName`, `newEmail` |
|
|
38
|
+
| `EmailExportReady` | `userName`, `downloadLink`, `expiresInHours`, `expiresIn`, `partial`, `failedSources` (the export sources that could not be included) |
|
|
39
|
+
|
|
40
|
+
`userName` is the user's name, or their email address when they have none. `expiresInHours` is the lifetime as a number, for the template to word in the recipient's language; `expiresIn` is the same lifetime already worded in English (`24 hours`), kept for templates written before `expiresInHours`.
|
|
41
|
+
|
|
42
|
+
## Rendered examples
|
|
43
|
+
|
|
44
|
+
These captures show HTML returned by the DMS frontend's real render endpoint, opened directly in Chromium, rather than the playground's HTML code viewer. All names, codes, addresses and links are fictitious; no email is sent. The default SVG logo is intentionally omitted by `publicLogoOnly`.
|
|
45
|
+
|
|
46
|
+
### EmailUserValidation
|
|
47
|
+
|
|
48
|
+
The signup email asks Alex to verify their address with a six-digit code, valid for 24 hours.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
### EmailResetPassword
|
|
53
|
+
|
|
54
|
+
The password-reset email uses a separate code and the configured recovery lifetime: two hours in this example.
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
### EmailTwoFactor
|
|
59
|
+
|
|
60
|
+
The second-factor email provides a sign-in code. Its footer tells the recipient to ignore the message if they did not attempt to sign in.
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
### EmailAdminInvite
|
|
65
|
+
|
|
66
|
+
The invitation names the inviter, workspace and platform when they are known. Its button uses a fictitious destination here.
|
|
67
|
+
|
|
68
|
+

|
|
69
|
+
|
|
70
|
+
### EmailChangeVerification
|
|
71
|
+
|
|
72
|
+
The new address receives the code that confirms the sign-in email change.
|
|
73
|
+
|
|
74
|
+

|
|
75
|
+
|
|
76
|
+
### EmailChangeNotice
|
|
77
|
+
|
|
78
|
+
The current address receives a notice, not the confirmation code. It tells the recipient what to do if they did not request the change.
|
|
79
|
+
|
|
80
|
+

|
|
81
|
+
|
|
82
|
+
### EmailExportReady
|
|
83
|
+
|
|
84
|
+
A complete export provides a download button and an expiry. A partial export also names the sources that could not be included.
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
These are browser previews, not Gmail, Outlook or Apple Mail compatibility tests. See [Email Layout](/docs/interfaces/html-render/introduction#email-layout) for the custom digest, logo variants and the limits of dark-mode support.
|
|
91
|
+
|
|
92
|
+
## Language
|
|
93
|
+
|
|
94
|
+
Each email is written in the recipient's language: the account's `language`, or for an invitation the language it was sent in. English and French are supported; any other language falls back to English.
|
|
95
|
+
|
|
96
|
+
The body text lives in the `dms-layout` locale files (`i18n/locales/layout-*.json`), under one key per email (`emails.user_validation`, `emails.reset_password`, `emails.two_factor`, `emails.admin_invite`, `emails.email_change_verification`, `emails.email_change_notice`, `emails.export_ready`) plus the shared `emails.common`. Locale files merge in module priority order, so a module registered with a priority above the DMS's (`0`) can reword any of these keys in its own `i18n/locales/*.json`:
|
|
97
|
+
|
|
98
|
+
```json [frontend-vue/i18n/locales/acme-en-GB.json]
|
|
99
|
+
{
|
|
100
|
+
"emails": {
|
|
101
|
+
"reset_password": {
|
|
102
|
+
"footer": "Didn't ask for this? Contact support@acme.test."
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The subject is set by the backend, which sends it in the same language. It cannot be reworded from a module.
|
|
109
|
+
|
|
110
|
+
## Logo
|
|
111
|
+
|
|
112
|
+
Every built-in email draws `branding.logo.email.light` from the app config, or `branding.logo.default.light` when no email logo is set (see [Email Layout](/docs/interfaces/html-render/introduction#email-layout)), with `publicLogoOnly` on. The logo shows only when a mail client can load it: a PNG or JPEG served from a public `http(s)` address. The default DMS logo is an SVG, which Gmail and Outlook refuse, so out of the box the emails carry no logo. To show yours, set an `email` logo in your layer's `app/app.config.ts`, and serve the dashboard from a public `clientBaseUrl`:
|
|
113
|
+
|
|
114
|
+
```ts [frontend-vue/app/app.config.ts]
|
|
115
|
+
export default defineDmsAppConfig({
|
|
116
|
+
branding: {
|
|
117
|
+
logo: {
|
|
118
|
+
email: {
|
|
119
|
+
light: "/branding/logo-email.png",
|
|
120
|
+
dark: "/branding/logo-email-dark.png",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The loader hands every layer's `app/app.config.ts` to the email renderer too (`@antelopejs/dms-frontend` 0.6.0 or later), merged in module priority order, so a module registered with a priority above the DMS's (`0`) wins. `dark` is optional: when it passes the same check and differs from `light`, the header becomes a `<picture>` whose dark source the mail clients that follow the reader's dark mode pick, and the others draw `light`.
|
|
128
|
+
|
|
129
|
+
## Replacing a built-in email
|
|
130
|
+
|
|
131
|
+
The render server looks a template up by its file name, in module priority order, and renders the first match. A module registered with a `priority` above the DMS's (`0`, see [`AddFrontendModule`](/docs/interfaces/core/pages-and-modules#addfrontendmoduleoptions)) therefore replaces a built-in email by shipping a component of the same name in its `dms.email.ts`:
|
|
132
|
+
|
|
133
|
+
```ts [frontend-vue/dms.email.ts]
|
|
134
|
+
export const serverEmailTemplates = import.meta.glob("./app/emails/**/*.vue");
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```vue [frontend-vue/app/emails/EmailResetPassword.vue]
|
|
138
|
+
<script setup lang="ts">
|
|
139
|
+
import { useI18n } from "vue-i18n";
|
|
140
|
+
import EmailLayout from "#dms-layout/app/emails/components/EmailLayout.vue";
|
|
141
|
+
import EmailOTP from "#dms-layout/app/emails/components/EmailOTP.vue";
|
|
142
|
+
|
|
143
|
+
defineProps<{ userName: string; resetCode: string; expiresInHours: number }>();
|
|
144
|
+
|
|
145
|
+
const { t, locale } = useI18n();
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<template>
|
|
149
|
+
<EmailLayout :title="t('emails.reset_password.title')" :lang="locale" public-logo-only>
|
|
150
|
+
<EText>{{ t("emails.common.greeting", { name: userName }) }}</EText>
|
|
151
|
+
<EText>Use this code to choose a new Acme password:</EText>
|
|
152
|
+
<EmailOTP :code="resetCode" :expires-in="t('emails.common.hours', expiresInHours)" />
|
|
153
|
+
</EmailLayout>
|
|
154
|
+
</template>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The replacement receives the props listed in [Props](#props) and keeps the backend's subject. Do not register the name again with `RegisterHtmlTemplate`: the DMS already sends it. At equal priority the first module by id wins, so set the priority explicitly rather than relying on it.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|