@budibase/worker 3.13.10 → 3.13.11
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/package.json +4 -3
- package/src/api/controllers/global/templates.ts +67 -2
- package/src/api/routes/global/templates.ts +1 -0
- package/src/api/routes/global/tests/templates.spec.ts +91 -1
- package/src/constants/templates/index.ts +74 -1
- package/src/constants/templates/tests/email_templates.yaml +523 -0
- package/src/constants/templates/tests/email_templates_empty.yaml +0 -0
- package/src/constants/templates/tests/email_templates_invalid.yaml +4 -0
- package/src/constants/templates/tests/templates.spec.ts +174 -0
- package/src/environment.ts +1 -0
- package/src/index.ts +6 -0
- package/src/tests/api/templates.ts +12 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "3.13.
|
|
4
|
+
"version": "3.13.11",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"pouchdb": "7.3.0",
|
|
69
69
|
"pouchdb-all-dbs": "1.1.1",
|
|
70
70
|
"server-destroy": "1.0.1",
|
|
71
|
-
"uuid": "^8.3.2"
|
|
71
|
+
"uuid": "^8.3.2",
|
|
72
|
+
"yaml": "^2.8.0"
|
|
72
73
|
},
|
|
73
74
|
"devDependencies": {
|
|
74
75
|
"@jest/types": "^29.6.3",
|
|
@@ -113,5 +114,5 @@
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
},
|
|
116
|
-
"gitHead": "
|
|
117
|
+
"gitHead": "48114cd1183da82668db193e89c7dd708e1eb5fa"
|
|
117
118
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { v4 } from "uuid"
|
|
1
2
|
import {
|
|
2
3
|
TemplateMetadata,
|
|
3
4
|
TemplateBindings,
|
|
4
5
|
GLOBAL_OWNER,
|
|
5
6
|
} from "../../../constants"
|
|
6
|
-
import {
|
|
7
|
-
|
|
7
|
+
import {
|
|
8
|
+
addBaseTemplates,
|
|
9
|
+
getTemplateByID,
|
|
10
|
+
getTemplates,
|
|
11
|
+
} from "../../../constants/templates"
|
|
12
|
+
import { tenancy, db as dbCore, objectStore } from "@budibase/backend-core"
|
|
8
13
|
import {
|
|
9
14
|
DeleteGlobalTemplateResponse,
|
|
10
15
|
FetchGlobalTemplateByOwnerIDResponse,
|
|
@@ -17,7 +22,12 @@ import {
|
|
|
17
22
|
GlobalTemplateBinding,
|
|
18
23
|
GlobalTemplateDefinition,
|
|
19
24
|
UserCtx,
|
|
25
|
+
Ctx,
|
|
26
|
+
EmailTemplatePurpose,
|
|
20
27
|
} from "@budibase/types"
|
|
28
|
+
import { join } from "path"
|
|
29
|
+
import fs from "fs"
|
|
30
|
+
import yaml from "yaml"
|
|
21
31
|
|
|
22
32
|
export async function save(
|
|
23
33
|
ctx: UserCtx<SaveGlobalTemplateRequest, SaveGlobalTemplateResponse>
|
|
@@ -93,3 +103,58 @@ export async function destroy(
|
|
|
93
103
|
await db.remove(ctx.params.id, ctx.params.rev)
|
|
94
104
|
ctx.body = { message: `Template ${ctx.params.id} deleted.` }
|
|
95
105
|
}
|
|
106
|
+
|
|
107
|
+
function templatesToYaml(
|
|
108
|
+
templates: Array<{ contents: string; purpose: string }>
|
|
109
|
+
): string {
|
|
110
|
+
const doc = new yaml.Document()
|
|
111
|
+
|
|
112
|
+
// Build the template object
|
|
113
|
+
const templatesObj: Record<string, yaml.Scalar> = {}
|
|
114
|
+
templates.forEach(template => {
|
|
115
|
+
const scalar = new yaml.Scalar(template.contents)
|
|
116
|
+
scalar.type = "BLOCK_LITERAL"
|
|
117
|
+
templatesObj[template.purpose] = scalar
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Set the document contents
|
|
121
|
+
doc.contents = doc.createNode({ templates: templatesObj })
|
|
122
|
+
|
|
123
|
+
return doc.toString()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function exportTemplates(ctx: Ctx) {
|
|
127
|
+
const { params } = ctx
|
|
128
|
+
|
|
129
|
+
// Limited to email for now
|
|
130
|
+
if (params.type === "email") {
|
|
131
|
+
// default|custom
|
|
132
|
+
const { type = "default" } = ctx.request?.body || {}
|
|
133
|
+
|
|
134
|
+
// Load either original or customised templates
|
|
135
|
+
const allTemplates =
|
|
136
|
+
type === "custom" ? await getTemplates() : addBaseTemplates([], "email")
|
|
137
|
+
|
|
138
|
+
const filtered = allTemplates.filter(
|
|
139
|
+
({ purpose }) => purpose !== EmailTemplatePurpose.CORE
|
|
140
|
+
)
|
|
141
|
+
// Outline all the type keys
|
|
142
|
+
const header = `# Template types: ${filtered.map(t => t.purpose).join(", ")} :) \n\n`
|
|
143
|
+
const yamlContent = templatesToYaml(filtered)
|
|
144
|
+
try {
|
|
145
|
+
// Combine header and content
|
|
146
|
+
const finalYaml = header + yamlContent
|
|
147
|
+
ctx.attachment(
|
|
148
|
+
`${type === "custom" ? "bb_email_templates" : "bb_default_email_templates"}.yaml`
|
|
149
|
+
)
|
|
150
|
+
const path = join(objectStore.budibaseTempDir(), v4())
|
|
151
|
+
fs.writeFileSync(path, finalYaml)
|
|
152
|
+
ctx.body = fs.createReadStream(path)
|
|
153
|
+
} catch (err: any) {
|
|
154
|
+
ctx.throw(
|
|
155
|
+
err.status,
|
|
156
|
+
`Could not download email templates: ${err.message}`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -33,6 +33,7 @@ router
|
|
|
33
33
|
.get("/api/global/template/:type", controller.fetchByType)
|
|
34
34
|
.get("/api/global/template/:ownerId", controller.fetchByOwner)
|
|
35
35
|
.get("/api/global/template/:id", controller.find)
|
|
36
|
+
.post("/api/global/template/:type/export", controller.exportTemplates)
|
|
36
37
|
.delete("/api/global/template/:id/:rev", adminOnly, controller.destroy)
|
|
37
38
|
|
|
38
39
|
export default router
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { TemplateMetadata, TemplateType } from "../../../../constants"
|
|
2
2
|
import { TestConfiguration } from "../../../../tests"
|
|
3
|
-
import { EmailTemplatePurpose } from "@budibase/types"
|
|
3
|
+
import { EmailTemplatePurpose, Template } from "@budibase/types"
|
|
4
|
+
import { addBaseTemplates } from "../../../../constants/templates"
|
|
5
|
+
import { tenancy } from "@budibase/backend-core"
|
|
6
|
+
import yaml from "yaml"
|
|
4
7
|
|
|
5
8
|
// TODO
|
|
6
9
|
|
|
@@ -101,4 +104,91 @@ describe("/api/global/template", () => {
|
|
|
101
104
|
).toBeDefined()
|
|
102
105
|
})
|
|
103
106
|
})
|
|
107
|
+
|
|
108
|
+
describe("POST /api/global/template/:type/export - emails", () => {
|
|
109
|
+
let templates: Template[] = []
|
|
110
|
+
|
|
111
|
+
beforeAll(async () => {
|
|
112
|
+
addBaseTemplates(templates, "email")
|
|
113
|
+
|
|
114
|
+
// Core is not an updatable template and should never be included
|
|
115
|
+
templates = templates.filter(t => t.purpose !== EmailTemplatePurpose.CORE)
|
|
116
|
+
|
|
117
|
+
await config.beforeAll()
|
|
118
|
+
|
|
119
|
+
const templateResp = await config.api.templates.getTemplate()
|
|
120
|
+
const templateDocs = templateResp.body.filter((t: Template) => t._id)
|
|
121
|
+
|
|
122
|
+
// Purge all docs from the previous test blocks
|
|
123
|
+
await config.doInTenant(async () => {
|
|
124
|
+
const db = tenancy.getGlobalDB()
|
|
125
|
+
await db.bulkRemove(templateDocs)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("should export all default email templates as yaml", async () => {
|
|
130
|
+
const resp = await config.api.templates.exportTemplates()
|
|
131
|
+
|
|
132
|
+
expect(resp.type).toBe("text/yaml")
|
|
133
|
+
|
|
134
|
+
const doc = yaml.parse(resp.text)
|
|
135
|
+
|
|
136
|
+
// Confirm the export file name
|
|
137
|
+
const disposition = resp.header["content-disposition"]
|
|
138
|
+
expect(disposition).toContain("bb_default_email_templates")
|
|
139
|
+
|
|
140
|
+
// All templates type should be present and match the hbs contents.
|
|
141
|
+
const processedTemplates = templates.filter(t => {
|
|
142
|
+
const yamlTemplateContents = doc.templates[t.purpose]
|
|
143
|
+
return t.contents === yamlTemplateContents
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(processedTemplates.length).toBe(templates.length)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("should export user edited email templates", async () => {
|
|
150
|
+
const customContent = "<div>hello</div>"
|
|
151
|
+
|
|
152
|
+
// Alter one of the templates. This creates 1 couch doc, for the custom type
|
|
153
|
+
// All the rest will still contain content from the static hbs files
|
|
154
|
+
await config.api.templates.saveTemplate({
|
|
155
|
+
purpose: EmailTemplatePurpose.CUSTOM,
|
|
156
|
+
contents: customContent,
|
|
157
|
+
type: "email",
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const resp = await config.api.templates.exportTemplates({
|
|
161
|
+
data: {
|
|
162
|
+
type: "custom",
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(resp.type).toBe("text/yaml")
|
|
167
|
+
|
|
168
|
+
const doc = yaml.parse(resp.text)
|
|
169
|
+
|
|
170
|
+
const disposition = resp.header["content-disposition"]
|
|
171
|
+
expect(disposition).toContain("bb_email_templates.yaml")
|
|
172
|
+
|
|
173
|
+
// All templates should be present and match both the static and custom contents
|
|
174
|
+
const processedTemplates = templates.filter(t => {
|
|
175
|
+
const yamlTemplateContents = doc.templates[t.purpose]
|
|
176
|
+
// Check the custom contents has been updated
|
|
177
|
+
if (t.purpose === EmailTemplatePurpose.CUSTOM) {
|
|
178
|
+
return yamlTemplateContents === customContent
|
|
179
|
+
}
|
|
180
|
+
return t.contents === yamlTemplateContents
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
expect(processedTemplates.length).toBe(templates.length)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("should ignore non-email requests", async () => {
|
|
187
|
+
// Only email templates are supported.
|
|
188
|
+
await config.api.templates.exportTemplates(
|
|
189
|
+
{ type: "something" },
|
|
190
|
+
{ status: 404 }
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
104
194
|
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readStaticFile } from "../../utilities/fileSystem"
|
|
2
2
|
import { TemplateType, TemplatePurpose, GLOBAL_OWNER } from "../index"
|
|
3
3
|
import { join } from "path"
|
|
4
|
-
import { db as dbCore, tenancy } from "@budibase/backend-core"
|
|
4
|
+
import { db as dbCore, tenancy, context } from "@budibase/backend-core"
|
|
5
5
|
import { Template, EmailTemplatePurpose } from "@budibase/types"
|
|
6
|
+
import yaml from "yaml"
|
|
6
7
|
|
|
7
8
|
export const EmailTemplates = {
|
|
8
9
|
[EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile(
|
|
@@ -19,6 +20,78 @@ export const EmailTemplates = {
|
|
|
19
20
|
[EmailTemplatePurpose.CORE]: readStaticFile(join(__dirname, "core.hbs")),
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
export const removeWhitespaceBetweenTags = (content: string) => {
|
|
24
|
+
return content.replace(/>\s+</g, "><").replace(/\s+/g, " ").trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loadTemplateConfig(pathTo: string) {
|
|
28
|
+
let templateConfig: Record<string, string>
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
console.log("Loading email templates:", pathTo)
|
|
32
|
+
const config = readStaticFile(pathTo)
|
|
33
|
+
const parsed = yaml.parse(config)
|
|
34
|
+
|
|
35
|
+
if (!parsed || !parsed.templates) {
|
|
36
|
+
console.log(`No email templates found: ${pathTo}`)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Remove core as it contains licensed content
|
|
41
|
+
// Unlikely, but just to be certain
|
|
42
|
+
const { core, ...rest } = parsed.templates
|
|
43
|
+
templateConfig = rest
|
|
44
|
+
} catch (e: any) {
|
|
45
|
+
console.log("There was a problem parsing email templates: ", e.message)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await context.doInTenant(tenancy.getTenantId(), async () => {
|
|
50
|
+
try {
|
|
51
|
+
const templates = await getTemplates({ type: "email" })
|
|
52
|
+
|
|
53
|
+
const updates: Template[] = templates.reduce(
|
|
54
|
+
(acc: Template[], template: Template) => {
|
|
55
|
+
const config = templateConfig[template.purpose]
|
|
56
|
+
if (config) {
|
|
57
|
+
// Need to normalise the text spacing for comparison
|
|
58
|
+
const configContent = removeWhitespaceBetweenTags(config)
|
|
59
|
+
const templateContent = removeWhitespaceBetweenTags(
|
|
60
|
+
template.contents
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// If the template is exactly the same, ignore it.
|
|
64
|
+
if (configContent === templateContent) return acc
|
|
65
|
+
const owner = template.ownerId || GLOBAL_OWNER
|
|
66
|
+
// Create/Update template docs
|
|
67
|
+
acc.push({
|
|
68
|
+
...template,
|
|
69
|
+
contents: config,
|
|
70
|
+
...(!template._id
|
|
71
|
+
? { _id: dbCore.generateTemplateID(owner) }
|
|
72
|
+
: {}),
|
|
73
|
+
ownerId: owner,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
return acc
|
|
77
|
+
},
|
|
78
|
+
[]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if (updates.length) {
|
|
82
|
+
const updateList = updates.map(u => u.purpose)
|
|
83
|
+
const db = tenancy.getGlobalDB()
|
|
84
|
+
await db.bulkDocs(updates)
|
|
85
|
+
console.log(`Email templates updated: ${updateList.join(",")}`)
|
|
86
|
+
} else {
|
|
87
|
+
console.log(`Email templates unchanged`)
|
|
88
|
+
}
|
|
89
|
+
} catch (e: any) {
|
|
90
|
+
console.log("Error persisting email templates", e.message)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
22
95
|
export function addBaseTemplates(templates: Template[], type?: string) {
|
|
23
96
|
let purposeList
|
|
24
97
|
switch (type) {
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# Template types: base, password_recovery, invitation, welcome, custom :)
|
|
2
|
+
|
|
3
|
+
templates:
|
|
4
|
+
base: |-
|
|
5
|
+
<!-- Based on templates: https://github.com/wildbit/postmark-templates/blob/master/templates/plain -->
|
|
6
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
7
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
8
|
+
<head>
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10
|
+
<meta name="x-apple-disable-message-reformatting" />
|
|
11
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
12
|
+
<meta name="color-scheme" content="light dark" />
|
|
13
|
+
<meta name="supported-color-schemes" content="light dark" />
|
|
14
|
+
<title></title>
|
|
15
|
+
<style type="text/css" rel="stylesheet" media="all">
|
|
16
|
+
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap');
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
width: 100% !important;
|
|
20
|
+
height: 100%;
|
|
21
|
+
margin: 0;
|
|
22
|
+
-webkit-text-size-adjust: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
a {
|
|
26
|
+
color: #6E56FF !important;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
a img {
|
|
30
|
+
border: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
td {
|
|
34
|
+
word-break: break-word;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.preheader {
|
|
38
|
+
display: none !important;
|
|
39
|
+
visibility: hidden;
|
|
40
|
+
mso-hide: all;
|
|
41
|
+
font-size: 1px;
|
|
42
|
+
line-height: 1px;
|
|
43
|
+
max-height: 0;
|
|
44
|
+
max-width: 0;
|
|
45
|
+
opacity: 0;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Type ------------------------------ */
|
|
50
|
+
|
|
51
|
+
body,
|
|
52
|
+
td,
|
|
53
|
+
th {
|
|
54
|
+
font-family: "Source Sans 3", Helvetica, Arial, sans-serif;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
h1 {
|
|
58
|
+
margin-top: 0;
|
|
59
|
+
color: #333333;
|
|
60
|
+
font-size: 22px;
|
|
61
|
+
font-weight: bold;
|
|
62
|
+
text-align: left;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
h2 {
|
|
66
|
+
margin-top: 0;
|
|
67
|
+
color: #333333;
|
|
68
|
+
font-size: 16px;
|
|
69
|
+
font-weight: bold;
|
|
70
|
+
text-align: left;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
h3 {
|
|
74
|
+
margin-top: 0;
|
|
75
|
+
color: #333333;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: bold;
|
|
78
|
+
text-align: left;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
td,
|
|
82
|
+
th {
|
|
83
|
+
font-size: 16px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
p,
|
|
87
|
+
ul,
|
|
88
|
+
ol,
|
|
89
|
+
blockquote {
|
|
90
|
+
margin: .4em 0 1.1875em;
|
|
91
|
+
font-size: 16px;
|
|
92
|
+
line-height: 1.625;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
p.sub {
|
|
96
|
+
font-size: 13px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Utilities ------------------------------ */
|
|
100
|
+
|
|
101
|
+
.align-right {
|
|
102
|
+
text-align: right;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.align-left {
|
|
106
|
+
text-align: left;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.align-center {
|
|
110
|
+
text-align: center;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Buttons ------------------------------ */
|
|
114
|
+
|
|
115
|
+
.button {
|
|
116
|
+
background-color: #6E56FF;
|
|
117
|
+
border-top: 10px solid #6E56FF;
|
|
118
|
+
border-right: 18px solid #6E56FF;
|
|
119
|
+
border-bottom: 10px solid #6E56FF;
|
|
120
|
+
border-left: 18px solid #6E56FF;
|
|
121
|
+
display: inline-block;
|
|
122
|
+
color: #FFF !important;
|
|
123
|
+
text-decoration: none !important;
|
|
124
|
+
border-radius: 3px;
|
|
125
|
+
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
|
126
|
+
-webkit-text-size-adjust: none;
|
|
127
|
+
box-sizing: border-box;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.button--green {
|
|
131
|
+
background-color: #22BC66;
|
|
132
|
+
border-top: 10px solid #22BC66;
|
|
133
|
+
border-right: 18px solid #22BC66;
|
|
134
|
+
border-bottom: 10px solid #22BC66;
|
|
135
|
+
border-left: 18px solid #22BC66;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.button--red {
|
|
139
|
+
background-color: #FF6136;
|
|
140
|
+
border-top: 10px solid #FF6136;
|
|
141
|
+
border-right: 18px solid #FF6136;
|
|
142
|
+
border-bottom: 10px solid #FF6136;
|
|
143
|
+
border-left: 18px solid #FF6136;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@media only screen and (max-width: 500px) {
|
|
147
|
+
.button {
|
|
148
|
+
width: 100% !important;
|
|
149
|
+
text-align: center !important;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Attribute list ------------------------------ */
|
|
154
|
+
|
|
155
|
+
.attributes {
|
|
156
|
+
margin: 0 0 21px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.attributes_content {
|
|
160
|
+
background-color: #F4F4F7;
|
|
161
|
+
padding: 16px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.attributes_item {
|
|
165
|
+
padding: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Related Items ------------------------------ */
|
|
169
|
+
|
|
170
|
+
.related {
|
|
171
|
+
width: 100%;
|
|
172
|
+
margin: 0;
|
|
173
|
+
padding: 25px 0 0 0;
|
|
174
|
+
-premailer-width: 100%;
|
|
175
|
+
-premailer-cellpadding: 0;
|
|
176
|
+
-premailer-cellspacing: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.related_item {
|
|
180
|
+
padding: 10px 0;
|
|
181
|
+
color: #CBCCCF;
|
|
182
|
+
font-size: 15px;
|
|
183
|
+
line-height: 18px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.related_item-title {
|
|
187
|
+
display: block;
|
|
188
|
+
margin: .5em 0 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.related_item-thumb {
|
|
192
|
+
display: block;
|
|
193
|
+
padding-bottom: 10px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.related_heading {
|
|
197
|
+
border-top: 1px solid #CBCCCF;
|
|
198
|
+
text-align: center;
|
|
199
|
+
padding: 25px 0 10px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Discount Code ------------------------------ */
|
|
203
|
+
|
|
204
|
+
.discount {
|
|
205
|
+
width: 100%;
|
|
206
|
+
margin: 0;
|
|
207
|
+
padding: 24px;
|
|
208
|
+
-premailer-width: 100%;
|
|
209
|
+
-premailer-cellpadding: 0;
|
|
210
|
+
-premailer-cellspacing: 0;
|
|
211
|
+
background-color: #F4F4F7;
|
|
212
|
+
border: 2px dashed #CBCCCF;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.discount_heading {
|
|
216
|
+
text-align: center;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.discount_body {
|
|
220
|
+
text-align: center;
|
|
221
|
+
font-size: 15px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Social Icons ------------------------------ */
|
|
225
|
+
|
|
226
|
+
.social {
|
|
227
|
+
width: auto;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.social td {
|
|
231
|
+
padding: 0;
|
|
232
|
+
width: auto;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.social_icon {
|
|
236
|
+
height: 20px;
|
|
237
|
+
margin: 0 8px 10px 8px;
|
|
238
|
+
padding: 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* Data table ------------------------------ */
|
|
242
|
+
|
|
243
|
+
.purchase {
|
|
244
|
+
width: 100%;
|
|
245
|
+
margin: 0;
|
|
246
|
+
padding: 35px 0;
|
|
247
|
+
-premailer-width: 100%;
|
|
248
|
+
-premailer-cellpadding: 0;
|
|
249
|
+
-premailer-cellspacing: 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.purchase_content {
|
|
253
|
+
width: 100%;
|
|
254
|
+
margin: 0;
|
|
255
|
+
padding: 25px 0 0 0;
|
|
256
|
+
-premailer-width: 100%;
|
|
257
|
+
-premailer-cellpadding: 0;
|
|
258
|
+
-premailer-cellspacing: 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.purchase_item {
|
|
262
|
+
padding: 10px 0;
|
|
263
|
+
color: #51545E;
|
|
264
|
+
font-size: 15px;
|
|
265
|
+
line-height: 18px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.purchase_heading {
|
|
269
|
+
padding-bottom: 8px;
|
|
270
|
+
border-bottom: 1px solid #EAEAEC;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.purchase_heading p {
|
|
274
|
+
margin: 0;
|
|
275
|
+
color: #85878E;
|
|
276
|
+
font-size: 12px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.purchase_footer {
|
|
280
|
+
padding-top: 15px;
|
|
281
|
+
border-top: 1px solid #EAEAEC;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.purchase_total {
|
|
285
|
+
margin: 0;
|
|
286
|
+
text-align: right;
|
|
287
|
+
font-weight: bold;
|
|
288
|
+
color: #333333;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.purchase_total--label {
|
|
292
|
+
padding: 0 15px 0 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
body {
|
|
296
|
+
background-color: #FFF;
|
|
297
|
+
color: #333;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
p {
|
|
301
|
+
color: #333;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.email-wrapper {
|
|
305
|
+
width: 100%;
|
|
306
|
+
margin: 0;
|
|
307
|
+
padding: 0;
|
|
308
|
+
-premailer-width: 100%;
|
|
309
|
+
-premailer-cellpadding: 0;
|
|
310
|
+
-premailer-cellspacing: 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.email-content {
|
|
314
|
+
width: 100%;
|
|
315
|
+
margin: 0;
|
|
316
|
+
padding: 0;
|
|
317
|
+
-premailer-width: 100%;
|
|
318
|
+
-premailer-cellpadding: 0;
|
|
319
|
+
-premailer-cellspacing: 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Masthead ----------------------- */
|
|
323
|
+
|
|
324
|
+
.email-masthead {
|
|
325
|
+
padding: 25px 0;
|
|
326
|
+
text-align: center;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.email-masthead_logo {
|
|
330
|
+
width: 94px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.email-masthead_name {
|
|
334
|
+
font-size: 16px;
|
|
335
|
+
font-weight: bold;
|
|
336
|
+
color: #A8AAAF;
|
|
337
|
+
text-decoration: none;
|
|
338
|
+
text-shadow: 0 1px 0 white;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* Body ------------------------------ */
|
|
342
|
+
|
|
343
|
+
.email-body {
|
|
344
|
+
width: 100%;
|
|
345
|
+
margin: 0;
|
|
346
|
+
padding: 0;
|
|
347
|
+
-premailer-width: 100%;
|
|
348
|
+
-premailer-cellpadding: 0;
|
|
349
|
+
-premailer-cellspacing: 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.email-body_inner {
|
|
353
|
+
width: 570px;
|
|
354
|
+
margin: 0 auto;
|
|
355
|
+
padding: 0;
|
|
356
|
+
-premailer-width: 570px;
|
|
357
|
+
-premailer-cellpadding: 0;
|
|
358
|
+
-premailer-cellspacing: 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.email-footer {
|
|
362
|
+
width: 570px;
|
|
363
|
+
margin: 0 auto;
|
|
364
|
+
padding: 0;
|
|
365
|
+
-premailer-width: 570px;
|
|
366
|
+
-premailer-cellpadding: 0;
|
|
367
|
+
-premailer-cellspacing: 0;
|
|
368
|
+
text-align: center;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.email-footer p {
|
|
372
|
+
color: #A8AAAF;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.body-action {
|
|
376
|
+
width: 100%;
|
|
377
|
+
margin: 30px auto;
|
|
378
|
+
padding: 0;
|
|
379
|
+
-premailer-width: 100%;
|
|
380
|
+
-premailer-cellpadding: 0;
|
|
381
|
+
-premailer-cellspacing: 0;
|
|
382
|
+
text-align: center;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.body-sub {
|
|
386
|
+
margin-top: 25px;
|
|
387
|
+
padding-top: 25px;
|
|
388
|
+
border-top: 1px solid #EAEAEC;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.content-cell {
|
|
392
|
+
padding: 35px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/*Media Queries ------------------------------ */
|
|
396
|
+
|
|
397
|
+
@media only screen and (max-width: 600px) {
|
|
398
|
+
.email-body_inner,
|
|
399
|
+
.email-footer {
|
|
400
|
+
width: 100% !important;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@media (prefers-color-scheme: dark) {
|
|
405
|
+
body {
|
|
406
|
+
background-color: #333333 !important;
|
|
407
|
+
color: #FFF !important;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
p,
|
|
411
|
+
ul,
|
|
412
|
+
ol,
|
|
413
|
+
blockquote,
|
|
414
|
+
h1,
|
|
415
|
+
h2,
|
|
416
|
+
h3,
|
|
417
|
+
span,
|
|
418
|
+
.purchase_item {
|
|
419
|
+
color: #FFF !important;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.attributes_content,
|
|
423
|
+
.discount {
|
|
424
|
+
background-color: #222 !important;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.email-masthead_name {
|
|
428
|
+
text-shadow: none !important;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
:root {
|
|
433
|
+
color-scheme: light dark;
|
|
434
|
+
supported-color-schemes: light dark;
|
|
435
|
+
}
|
|
436
|
+
</style>
|
|
437
|
+
<!--[if mso]>
|
|
438
|
+
<style type="text/css">
|
|
439
|
+
.f-fallback {
|
|
440
|
+
font-family: Arial, sans-serif;
|
|
441
|
+
}
|
|
442
|
+
</style>
|
|
443
|
+
<![endif]-->
|
|
444
|
+
</head>
|
|
445
|
+
<body>
|
|
446
|
+
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
447
|
+
<tr>
|
|
448
|
+
<td align="center">
|
|
449
|
+
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
450
|
+
<tr>
|
|
451
|
+
<td class="email-masthead">
|
|
452
|
+
<a href="{{ platformUrl }}" class="f-fallback email-masthead_name">
|
|
453
|
+
{{ company }}
|
|
454
|
+
</a>
|
|
455
|
+
</td>
|
|
456
|
+
</tr>
|
|
457
|
+
{{ body }}
|
|
458
|
+
<tr>
|
|
459
|
+
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
|
460
|
+
<tr>
|
|
461
|
+
<td class="content-cell" align="center">
|
|
462
|
+
<p class="f-fallback sub align-center">© {{ currentYear }} {{ company }}. All rights reserved.</p>
|
|
463
|
+
</td>
|
|
464
|
+
</tr>
|
|
465
|
+
</table>
|
|
466
|
+
</tr>
|
|
467
|
+
</table>
|
|
468
|
+
</td>
|
|
469
|
+
</tr>
|
|
470
|
+
</table>
|
|
471
|
+
</body>
|
|
472
|
+
</html>
|
|
473
|
+
password_recovery: |-
|
|
474
|
+
<!-- Based on template: https://github.com/wildbit/postmark-templates/blob/master/templates/plain/password-reset/content.html -->
|
|
475
|
+
<tr>
|
|
476
|
+
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
|
477
|
+
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
|
478
|
+
<!-- Body content -->
|
|
479
|
+
<tr>
|
|
480
|
+
<td class="content-cell">
|
|
481
|
+
<div class="f-fallback">
|
|
482
|
+
<h1>Hi {{#if name}}{{ name }}{{else}}{{ email }}{{/if}},</h1>
|
|
483
|
+
<p>You recently requested to reset your password for your {{ company }} account in your Budibase platform. Use the button below to reset it. <strong>This password reset is only valid for the next 24 hours.</strong></p>
|
|
484
|
+
<!-- Action -->
|
|
485
|
+
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
486
|
+
<tr>
|
|
487
|
+
<td align="center">
|
|
488
|
+
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
|
489
|
+
<tr>
|
|
490
|
+
<td align="center">
|
|
491
|
+
<a href="{{ resetUrl }}" class="f-fallback button button--green" target="_blank">Reset your password</a>
|
|
492
|
+
</td>
|
|
493
|
+
</tr>
|
|
494
|
+
</table>
|
|
495
|
+
</td>
|
|
496
|
+
</tr>
|
|
497
|
+
</table>
|
|
498
|
+
{{#if request}}
|
|
499
|
+
<p>For security, this request was received from a {{ request.os }} device.</p>
|
|
500
|
+
{{/if}}
|
|
501
|
+
<p>If you did not request a password reset, please ignore this email or contact support if you have questions.</p>
|
|
502
|
+
<p>Thanks,
|
|
503
|
+
<br>The {{ company }} Team</p>
|
|
504
|
+
<!-- Sub copy -->
|
|
505
|
+
<table class="body-sub" role="presentation">
|
|
506
|
+
<tr>
|
|
507
|
+
<td>
|
|
508
|
+
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
|
509
|
+
<p class="f-fallback sub">{{ resetUrl }}</p>
|
|
510
|
+
</td>
|
|
511
|
+
</tr>
|
|
512
|
+
</table>
|
|
513
|
+
</div>
|
|
514
|
+
</td>
|
|
515
|
+
</tr>
|
|
516
|
+
</table>
|
|
517
|
+
</td>
|
|
518
|
+
</tr>
|
|
519
|
+
welcome: "<div>welcome altered</div>"
|
|
520
|
+
custom: |-
|
|
521
|
+
<div>altered</div>
|
|
522
|
+
core: |-
|
|
523
|
+
I shouldn't be here
|
|
File without changes
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { TestConfiguration } from "../../../tests"
|
|
2
|
+
import { addBaseTemplates, loadTemplateConfig } from ".."
|
|
3
|
+
import { EmailTemplatePurpose, type Template } from "@budibase/types"
|
|
4
|
+
import { join } from "path"
|
|
5
|
+
|
|
6
|
+
describe("Loading yaml email templates", () => {
|
|
7
|
+
const config = new TestConfiguration()
|
|
8
|
+
let consoleSpy: jest.SpyInstance
|
|
9
|
+
let templates: Template[] = []
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
addBaseTemplates(templates, "email")
|
|
17
|
+
// Core is not an updatable template and should never be included
|
|
18
|
+
templates = templates.filter(t => t.purpose !== EmailTemplatePurpose.CORE)
|
|
19
|
+
|
|
20
|
+
await config.beforeAll()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await config.afterAll()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("should ignore an invalid path", async () => {
|
|
28
|
+
const testPath = join(__dirname, `./email_templates_wrong_path.yaml`)
|
|
29
|
+
|
|
30
|
+
await config.doInTenant(async () => {
|
|
31
|
+
loadTemplateConfig(testPath)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(consoleSpy).toHaveBeenCalledTimes(2)
|
|
35
|
+
|
|
36
|
+
// Confirm the error is relayed
|
|
37
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
38
|
+
"There was a problem parsing email templates: ",
|
|
39
|
+
expect.stringContaining("no such file")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
consoleSpy.mockRestore()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("should ignore empty contents", async () => {
|
|
46
|
+
// Valid path but its empty.
|
|
47
|
+
const testPath = join(__dirname, `./email_templates_empty.yaml`)
|
|
48
|
+
|
|
49
|
+
await config.doInTenant(async () => {
|
|
50
|
+
await loadTemplateConfig(testPath)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(consoleSpy).toHaveBeenCalledTimes(2)
|
|
54
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
55
|
+
`No email templates found: ${testPath}`
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
consoleSpy.mockRestore()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("should detect and ignore invalid yaml content", async () => {
|
|
62
|
+
const testPath = join(__dirname, `./email_templates_invalid.yaml`)
|
|
63
|
+
|
|
64
|
+
await config.doInTenant(async () => {
|
|
65
|
+
await loadTemplateConfig(testPath)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(consoleSpy).toHaveBeenCalledTimes(2)
|
|
69
|
+
// Confirm the error is relayed - there is an unclosed string in the test doc
|
|
70
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
71
|
+
"There was a problem parsing email templates: ",
|
|
72
|
+
expect.stringContaining("Missing closing")
|
|
73
|
+
)
|
|
74
|
+
consoleSpy.mockRestore()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("should process valid yaml content", async () => {
|
|
78
|
+
// welcome and custom modified, invitation was excluded
|
|
79
|
+
const testPath = join(__dirname, `./email_templates.yaml`)
|
|
80
|
+
|
|
81
|
+
const templatesBeforeResp = await config.api.templates.getTemplate()
|
|
82
|
+
const templatesBefore = templatesBeforeResp.body.reduce(
|
|
83
|
+
(acc: Record<string, any>, t: any) => {
|
|
84
|
+
if (t.purpose === EmailTemplatePurpose.CORE) return acc
|
|
85
|
+
acc[t.purpose] ??= t
|
|
86
|
+
return acc
|
|
87
|
+
},
|
|
88
|
+
{}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
await config.doInTenant(async () => {
|
|
92
|
+
await loadTemplateConfig(testPath)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const templatesUpdatedResp = await config.api.templates.getTemplate()
|
|
96
|
+
const templatesUpdated = templatesUpdatedResp.body.filter(
|
|
97
|
+
(t: any) => t.purpose !== EmailTemplatePurpose.CORE
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(templatesUpdated.length).toBe(templates.length)
|
|
101
|
+
|
|
102
|
+
// Confirm the changes have been applied and the templates are persisted
|
|
103
|
+
const processedTemplates = templatesUpdated.filter((t: any) => {
|
|
104
|
+
if (
|
|
105
|
+
(t._id &&
|
|
106
|
+
t.purpose === EmailTemplatePurpose.CUSTOM &&
|
|
107
|
+
t.contents === "<div>altered</div>") ||
|
|
108
|
+
(t._id &&
|
|
109
|
+
t.purpose === EmailTemplatePurpose.WELCOME &&
|
|
110
|
+
t.contents === "<div>welcome altered</div>")
|
|
111
|
+
) {
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
// All others are the same.
|
|
115
|
+
return templatesBefore[t.purpose].contents === t.contents
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(processedTemplates.length).toBe(templates.length)
|
|
119
|
+
|
|
120
|
+
// Should notify that the two templates that didn't match were in actually persisted.
|
|
121
|
+
expect(consoleSpy).toHaveBeenCalledTimes(2)
|
|
122
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
123
|
+
`Email templates updated: welcome,custom`
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
consoleSpy.mockRestore()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("should only update template docs when the contents has changed", async () => {
|
|
130
|
+
// welcome and custom modified, invitation was excluded
|
|
131
|
+
const testPath = join(__dirname, `./email_templates.yaml`)
|
|
132
|
+
|
|
133
|
+
const templatesBeforeResp = await config.api.templates.getTemplate()
|
|
134
|
+
const templatesBefore = templatesBeforeResp.body.reduce(
|
|
135
|
+
(acc: Record<string, any>, t: any) => {
|
|
136
|
+
if (t.purpose === EmailTemplatePurpose.CORE) return acc
|
|
137
|
+
acc[t.purpose] ??= t
|
|
138
|
+
return acc
|
|
139
|
+
},
|
|
140
|
+
{}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Update the templates. welcome and custom are persisted to couch
|
|
144
|
+
await config.doInTenant(async () => {
|
|
145
|
+
await loadTemplateConfig(testPath)
|
|
146
|
+
})
|
|
147
|
+
consoleSpy.mockReset()
|
|
148
|
+
|
|
149
|
+
// Run the update again. Simulating a restart of the worker.
|
|
150
|
+
await config.doInTenant(async () => {
|
|
151
|
+
await loadTemplateConfig(testPath)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const templatesUpdatedResp = await config.api.templates.getTemplate()
|
|
155
|
+
const templatesUpdated = templatesUpdatedResp.body.filter(
|
|
156
|
+
(t: any) => t.purpose !== EmailTemplatePurpose.CORE
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
expect(templatesUpdated.length).toBe(templates.length)
|
|
160
|
+
|
|
161
|
+
const processedTemplates = templatesUpdated.filter((t: any) => {
|
|
162
|
+
// All content unchanged
|
|
163
|
+
return templatesBefore[t.purpose].contents === t.contents
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(processedTemplates.length).toBe(templates.length)
|
|
167
|
+
|
|
168
|
+
expect(consoleSpy).toHaveBeenCalledTimes(2)
|
|
169
|
+
// Should confirm that on reprocessing, the templates were not persisted
|
|
170
|
+
expect(consoleSpy).toHaveBeenCalledWith(`Email templates unchanged`)
|
|
171
|
+
|
|
172
|
+
consoleSpy.mockRestore()
|
|
173
|
+
})
|
|
174
|
+
})
|
package/src/environment.ts
CHANGED
|
@@ -67,6 +67,7 @@ const environment = {
|
|
|
67
67
|
ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY,
|
|
68
68
|
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
|
69
69
|
TOP_LEVEL_PATH: TOP_LEVEL_PATH,
|
|
70
|
+
EMAIL_TEMPLATE_PATH: process.env.EMAIL_TEMPLATE_PATH,
|
|
70
71
|
/**
|
|
71
72
|
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
|
72
73
|
*/
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
features,
|
|
22
22
|
} from "@budibase/backend-core"
|
|
23
23
|
import RedisStore from "koa-redis"
|
|
24
|
+
import { loadTemplateConfig } from "./constants/templates"
|
|
24
25
|
|
|
25
26
|
db.init()
|
|
26
27
|
import koaBody from "koa-body"
|
|
@@ -131,6 +132,11 @@ export default server.listen(parseInt(env.PORT || "4002"), async () => {
|
|
|
131
132
|
await initPro()
|
|
132
133
|
await redis.clients.init()
|
|
133
134
|
features.init()
|
|
135
|
+
|
|
136
|
+
if (env.EMAIL_TEMPLATE_PATH) {
|
|
137
|
+
await loadTemplateConfig(env.EMAIL_TEMPLATE_PATH)
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
cache.docWritethrough.init()
|
|
135
141
|
// configure events to use the pro audit log write
|
|
136
142
|
// can't integrate directly into backend-core due to cyclic issues
|
|
@@ -22,4 +22,16 @@ export class TemplatesAPI extends TestAPI {
|
|
|
22
22
|
.set(opts?.headers ? opts.headers : this.config.defaultHeaders())
|
|
23
23
|
.expect(opts?.status ? opts.status : 200)
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
exportTemplates = (
|
|
27
|
+
req?: { type?: string; data?: any },
|
|
28
|
+
opts?: TestAPIOpts
|
|
29
|
+
) => {
|
|
30
|
+
const { type = "email", data } = req || {}
|
|
31
|
+
return this.request
|
|
32
|
+
.post(`/api/global/template/${type}/export`)
|
|
33
|
+
.send(data)
|
|
34
|
+
.set(opts?.headers ? opts.headers : this.config.defaultHeaders())
|
|
35
|
+
.expect(opts?.status ? opts.status : 200)
|
|
36
|
+
}
|
|
25
37
|
}
|