diff --git a/packages/worker/src/api/controllers/admin/templates.js b/packages/worker/src/api/controllers/admin/templates.js index d91323e0a1..ccf057c485 100644 --- a/packages/worker/src/api/controllers/admin/templates.js +++ b/packages/worker/src/api/controllers/admin/templates.js @@ -4,11 +4,32 @@ const { StaticDatabases, } = require("@budibase/auth").db const { CouchDB } = require("../../../db") -const { TemplatePurposePretty } = require("../../../constants") +const { TemplatePurposePretty, TemplateTypes, EmailTemplatePurpose, TemplatePurpose } = require("../../../constants") +const { getTemplateByPurpose } = require("../../../constants/templates") const GLOBAL_DB = StaticDatabases.GLOBAL.name const GLOBAL_OWNER = "global" +function addBaseTemplates(templates, type = null) { + let purposeList + switch (type) { + case TemplateTypes.EMAIL: + purposeList = Object.values(EmailTemplatePurpose) + break + default: + purposeList = Object.values(TemplatePurpose) + break + } + for (let purpose of purposeList) { + // check if a template exists already for purpose + if (templates.find(template => template.purpose === purpose)) { + continue + } + templates.push(getTemplateByPurpose(purpose)) + } + return templates +} + async function getTemplates({ ownerId, type, id } = {}) { const db = new CouchDB(GLOBAL_DB) const response = await db.allDocs( @@ -17,10 +38,15 @@ async function getTemplates({ ownerId, type, id } = {}) { }) ) let templates = response.rows.map(row => row.doc) + // should only be one template with ID + if (id) { + return templates[0] + } if (type) { templates = templates.filter(template => template.type === type) } - return templates + + return addBaseTemplates(templates, type) } exports.save = async ctx => { @@ -73,7 +99,8 @@ exports.find = async ctx => { } exports.destroy = async ctx => { - // TODO - // const db = new CouchDB(GLOBAL_DB) - ctx.body = {} + const db = new CouchDB(GLOBAL_DB) + await db.remove(ctx.params.id, ctx.params.rev) + ctx.message = `Template ${ctx.params.id} deleted.` + ctx.status = 200 } diff --git a/packages/worker/src/api/routes/admin/templates.js b/packages/worker/src/api/routes/admin/templates.js index 2207b72458..81bd814cbc 100644 --- a/packages/worker/src/api/routes/admin/templates.js +++ b/packages/worker/src/api/routes/admin/templates.js @@ -25,5 +25,5 @@ router .get("/api/admin/template", controller.fetch) .get("/api/admin/template/:type", controller.fetchByType) .get("/api/admin/template/:ownerId", controller.fetchByOwner) - .delete("/api/admin/template/:id", controller.destroy) .get("/api/admin/template/:id", controller.find) + .delete("/api/admin/template/:id/:rev", controller.destroy) diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.js index 5d52ce798f..8edba58fda 100644 --- a/packages/worker/src/constants/index.js +++ b/packages/worker/src/constants/index.js @@ -14,27 +14,52 @@ exports.Configs = { GOOGLE: "google", } -exports.TemplateTypes = { +const TemplateTypes = { EMAIL: "email", } -exports.TemplatePurpose = { +const EmailTemplatePurpose = { + HEADER: "header", + FOOTER: "footer", + STYLES: "styles", PASSWORD_RECOVERY: "password_recovery", INVITATION: "invitation", CUSTOM: "custom", } -exports.TemplatePurposePretty = [ - { - name: "Password Recovery", - value: exports.TemplatePurpose.PASSWORD_RECOVERY, - }, - { - name: "New User Invitation", - value: exports.TemplatePurpose.INVITATION, - }, - { - name: "Custom", - value: exports.TemplatePurpose.CUSTOM, - }, -] +const TemplatePurposePretty = { + [TemplateTypes.EMAIL]: [ + { + name: "Styling", + value: EmailTemplatePurpose.STYLES, + }, + { + name: "Header", + value: EmailTemplatePurpose.HEADER, + }, + { + name: "Footer", + value: EmailTemplatePurpose.FOOTER, + }, + { + name: "Password Recovery", + value: EmailTemplatePurpose.PASSWORD_RECOVERY + }, + { + name: "New User Invitation", + value: EmailTemplatePurpose.INVITATION, + }, + { + name: "Custom", + value: EmailTemplatePurpose.CUSTOM, + } + ] +} + +// all purpose combined +exports.TemplatePurpose = { + ...EmailTemplatePurpose, +} +exports.TemplateTypes = TemplateTypes +exports.EmailTemplatePurpose = EmailTemplatePurpose +exports.TemplatePurposePretty = TemplatePurposePretty diff --git a/packages/worker/src/constants/templates/footer.html b/packages/worker/src/constants/templates/footer.html new file mode 100644 index 0000000000..693fd3c0c0 --- /dev/null +++ b/packages/worker/src/constants/templates/footer.html @@ -0,0 +1,48 @@ + + + + + + + + +
\ No newline at end of file diff --git a/packages/worker/src/constants/templates/header.html b/packages/worker/src/constants/templates/header.html new file mode 100644 index 0000000000..7709bd30a8 --- /dev/null +++ b/packages/worker/src/constants/templates/header.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ \ No newline at end of file diff --git a/packages/worker/src/constants/templates/index.js b/packages/worker/src/constants/templates/index.js new file mode 100644 index 0000000000..42fbcf70ab --- /dev/null +++ b/packages/worker/src/constants/templates/index.js @@ -0,0 +1,19 @@ +const { readStaticFile } = require("../../utilities/fileSystem") +const { EmailTemplatePurpose } = require("../index") +const { join } = require("path") + +const TEMPLATE_PATH = join(__dirname, "..", "constants", "templates") + +exports.EmailTemplates = { + [EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile(join(TEMPLATE_PATH, "passwordRecovery.html")), + [EmailTemplatePurpose.INVITATION]: readStaticFile(join(TEMPLATE_PATH, "invitation.html")), + [EmailTemplatePurpose.HEADER]: readStaticFile(join(TEMPLATE_PATH, "header.html")), + [EmailTemplatePurpose.FOOTER]: readStaticFile(join(TEMPLATE_PATH, "footer.html")), + [EmailTemplatePurpose.STYLES]: readStaticFile(join(TEMPLATE_PATH, "style.css")), +} + +exports.getTemplateByPurpose = purpose => { + if (exports.EmailTemplates[purpose]) { + return exports.EmailTemplates[purpose] + } +} diff --git a/packages/worker/src/constants/templates/invitation.html b/packages/worker/src/constants/templates/invitation.html new file mode 100644 index 0000000000..8e154fe189 --- /dev/null +++ b/packages/worker/src/constants/templates/invitation.html @@ -0,0 +1,14 @@ + + + + + + +
\ No newline at end of file diff --git a/packages/worker/src/constants/templates/passwordRecovery.html b/packages/worker/src/constants/templates/passwordRecovery.html new file mode 100644 index 0000000000..e6b179ec81 --- /dev/null +++ b/packages/worker/src/constants/templates/passwordRecovery.html @@ -0,0 +1,15 @@ + + + + + + +
\ No newline at end of file diff --git a/packages/worker/src/constants/templates/style.css b/packages/worker/src/constants/templates/style.css new file mode 100644 index 0000000000..abcd797830 --- /dev/null +++ b/packages/worker/src/constants/templates/style.css @@ -0,0 +1,269 @@ +@font-face { + font-family: 'Playfair Display'; + font-style: italic; + font-weight: 400; + src: url(/fonts.gstatic.com/s/playfairdisplay/v22/nuFkD-vYSZviVYUb_rj3ij__anPXDTnohkk72xU.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +html, +body { + margin: 0 auto !important; + padding: 0 !important; + height: 100% !important; + width: 100% !important; + background: #f1f1f1; +} +* { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +div[style*="margin: 16px 0"] { + margin: 0 !important; +} +table, +td { + mso-table-lspace: 0pt !important; + mso-table-rspace: 0pt !important; +} +table { + border-spacing: 0 !important; + border-collapse: collapse !important; + table-layout: fixed !important; + margin: 0 auto !important; +} +img { + -ms-interpolation-mode:bicubic; +} +a { + text-decoration: none; +} +*[x-apple-data-detectors], /* iOS */ +.unstyle-auto-detected-links *, +.aBn { + border-bottom: 0 !important; + cursor: default !important; + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; +} +.a6S { + display: none !important; + opacity: 0.01 !important; +} +.im { + color: inherit !important; +} +img.g-img + div { + display: none !important; +} +@media only screen and (min-device-width: 320px) and (max-device-width: 374px) { + u ~ div .email-container { + min-width: 320px !important; + } +} +@media only screen and (min-device-width: 375px) and (max-device-width: 413px) { + u ~ div .email-container { + min-width: 375px !important; + } +} +@media only screen and (min-device-width: 414px) { + u ~ div .email-container { + min-width: 414px !important; + } +} +.primary{ + background: #f3a333; +} +.bg_white{ + background: #ffffff; +} +.bg_light{ + background: #fafafa; +} +.bg_black{ + background: #000000; +} +.bg_dark{ + background: rgba(0,0,0,.8); +} +.email-section{ + padding:2.5em; +} +.btn{ + padding: 10px 15px; +} +.btn.btn-primary{ + border-radius: 30px; + background: #f3a333; + color: #ffffff; +} +h1,h2,h3,h4,h5,h6{ + font-family: 'Playfair Display', serif; + color: #000000; + margin-top: 0; +} +body{ + font-family: 'Montserrat', sans-serif; + font-weight: 400; + font-size: 15px; + line-height: 1.8; + color: rgba(0,0,0,.4); +} +a{ + color: #f3a333; +} +table{ +} +.logo h1{ + margin: 0; +} +.logo h1 a{ + color: #000; + font-size: 20px; + font-weight: 700; + text-transform: uppercase; + font-family: 'Montserrat', sans-serif; +} +.hero{ + position: relative; +} +.hero img{ + +} +.hero .text{ + color: rgba(255,255,255,.8); +} +.hero .text h2{ + color: #ffffff; + font-size: 30px; + margin-bottom: 0; +} +.heading-section{ +} +.heading-section h2{ + color: #000000; + font-size: 28px; + margin-top: 0; + line-height: 1.4; +} +.heading-section .subheading{ + margin-bottom: 20px !important; + display: inline-block; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 2px; + color: rgba(0,0,0,.4); + position: relative; +} +.heading-section .subheading::after{ + position: absolute; + left: 0; + right: 0; + bottom: -10px; + content: ''; + width: 100%; + height: 2px; + background: #f3a333; + margin: 0 auto; +} +.heading-section-white{ + color: rgba(255,255,255,.8); +} +.heading-section-white h2{ + font-size: 28px; + line-height: 1; + padding-bottom: 0; +} +.heading-section-white h2{ + color: #ffffff; +} +.heading-section-white .subheading{ + margin-bottom: 0; + display: inline-block; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 2px; + color: rgba(255,255,255,.4); +} +.icon{ + text-align: center; +} +.icon img{ +} +.text-services{ + padding: 10px 10px 0; + text-align: center; +} +.text-services h3{ + font-size: 20px; +} +.text-services .meta{ + text-transform: uppercase; + font-size: 14px; +} +.img{ + width: 100%; + height: auto; + position: relative; +} +.img .icon{ + position: absolute; + top: 50%; + left: 0; + right: 0; + bottom: 0; + margin-top: -25px; +} +.img .icon a{ + display: block; + width: 60px; + position: absolute; + top: 0; + left: 50%; + margin-left: -25px; +} +.counter-text{ + text-align: center; +} +.counter-text .num{ + display: block; + color: #ffffff; + font-size: 34px; + font-weight: 700; +} +.counter-text .name{ + display: block; + color: rgba(255,255,255,.9); + font-size: 13px; +} +.footer{ + color: rgba(255,255,255,.5); +} +.footer .heading{ + color: #ffffff; + font-size: 20px; +} +.footer ul{ + margin: 0; + padding: 0; +} +.footer ul li{ + list-style: none; + margin-bottom: 10px; +} +.footer ul li a{ + color: rgba(255,255,255,1); +} +@media screen and (max-width: 500px) { + .icon{ + text-align: left; + } + .text-services{ + padding-left: 0; + padding-right: 20px; + text-align: left; + } +} \ No newline at end of file diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/worker/src/utilities/fileSystem.js b/packages/worker/src/utilities/fileSystem.js new file mode 100644 index 0000000000..7df21db695 --- /dev/null +++ b/packages/worker/src/utilities/fileSystem.js @@ -0,0 +1,5 @@ +const { readFileSync } = require("fs") + +exports.readStaticFile = path => { + return readFileSync(path, "utf-8") +} \ No newline at end of file