Website: Add script for creating HTML email partials from Markdown articles (#7574)

* add to-html-email

* email-preview

* add build-html-email script, update .gitignore

* email preview and email-templates pages, update routes, importer and policies

* add newsletter email layout, build-html-email script, to-html-email updates

* Update .gitignore

* add newsletter emails section to growth handbook

* update scripts

* update fonts in emails

* Update README.md

* Update build-html-email.js

* update comments, add page scripts to layouts

* revert change to build-static-content

* update script

* update email layout and helper

* update handbook entry

* update template data and helper

* update email layout

* update fake data and unsubscribe link

* move added handbook section to the marketing handbook

* rename script

* update colors in unused email template

* update capitalization in helper

* view-email-preview » view-email-template-preview

* Hide emails generated from markdown if they don't exist

* update preview page

* update markdown email directory, create sample article email

* lint fixes, add target="_blank" to links in emails

* update page layouts & styles

* update sample newsletter email, code font, newsletter layout

* Update README.md

* Update view-email-template-preview.js

Co-authored-by: Tim Kern <tim@fleetdm.com>
Co-authored-by: Mike Thomas <78363703+mike-j-thomas@users.noreply.github.com>
Co-authored-by: Mike McNeil <mikermcneil@users.noreply.github.com>
This commit is contained in:
Eric 2022-12-05 16:30:24 -06:00 committed by GitHub
parent 2c1ebe44bf
commit 2d0f33f369
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 991 additions and 8 deletions

View File

@ -422,6 +422,52 @@ Use "bcc" so recipients don't see each other's email addresses and send an email
`sails run deliver-release-announcement --emailAddresses='["foo@example.com","bar@example.com"]'`
### Newsletter emails
The content for our newsletter emails comes from our articles. Because our HTML emails require that the styles are added inline, we generate HTML emails by using a script and manually QA them before sending them out to subscribers.
#### Generating emails for the Fleet newsletter
To convert a Markdown article into an email for the newsletter, you'll need the following:
- A local copy of the [Fleet repo](https://github.com/fleetdm/fleet).
- [Node.js](https://nodejs.org/en/download/)
- (Optional) [Sails.js](https://sailsjs.com) installed globally on your machine (`npm install sails -g`)
Once you have the above follow these steps:
1. Open your terminal program, and navigate to the `website/` folder of your local copy of the Fleet repo.
>Note: If this is your first time running this script, you will need to run `npm install` inside of the `website/` folder to install the website's dependencies.
2. Run the `build-html-email` script and pass in the filename of the Markdown article you would like to convert with the `--articleFilename` flag.
- **With Node**, you will need to use `node ./node_modules/sails/bin/sails run build-html-email` to execute the script. e.g., `node ./node_modules/sails/bin/sails run build-html-email --articleFilename="fleet-4.19.0.md"`
- **With Sails.js installed globally** you can use `sails run build-html-email` to execute the script. e.g., `sails run build-html-email --articleFilename="fleet-4.19.0.md"`
> Note: Only Markdown (`.md`) files are supported by the build-html-email script. The file extension is optional when providing the articleFilename.
4. Once the script is complete, a new email partial will be added to the `website/views/emails/newsletter-partials/` folder.
> Note: If an email partial has already been created from the specified Markdown article, the old version will be overwritten by the new file.
5. Start the website server locally to preview the generated email. To test the changes locally, open a terminal window in the `website/` folder of the Fleet repo and run the following command:
- **With Node.js:** start the server by running `node ./node_modules/sails/bin/sails lift`.
- **With Sails.js installed globally:** start the server by running `sails lift`.
6. With the server lifted, navigate to http://localhost:2024/admin/email-preview and login with the test admin user credentials (email:`admin@example.com` pw: `abc123`).
Click on the generated email in the list of emails generated from Markdown content and navigate to the preview page. On this page, you can view the see how the email will look on a variety of screen sizes.
When you've made sure the content of the email looks good at all screen sizes, commit the new email partial to a new branch and open a pull request to add the file. You can request a review from a member of the digital experience team.
**Things to keep in mind when generating newsletter emails:**
- The emails will be generated using the Markdown file locally, any changes present in the local Markdown file will be reflected in the generated email.
- HTML elements in the Markdown file can cause rendering issues when previewing the generated email. If you see a "Script error" overlay while trying to preview an email, reach out to [Eric Shaw](https://github.com/eashaw) for help.
- The filename of the generated email will have periods changed to dashes. e.g., The generated email partial for `fleet-4.19.0.md` would be `fleet-4-19-0.ejs`
### Using Figma
We use Figma for most of our design work. This includes the Fleet product, our website, and our marketing collateral.

View File

@ -0,0 +1,133 @@
module.exports = {
friendlyName: 'View email template preview',
description: 'Display "email template preview" page.',
urlWildcardSuffix: 'template',
inputs: {
template: {
description: 'The path to an email template, specified in precisely the same way as the equivalent input of the sendTemplateEmail() helper.',
example: 'email-reset-password',
type: 'string',
required: true
},
raw: {
description: 'Whether to return the raw HTML for the email with no JS/CSS (rather than a personalized previewer web page.)',
extendedDescription: 'This can be used from an iframe to allow for accurately previewing email templates without worrying about style interference from the rest of the Sails app.',
type: 'boolean',
}
},
exits: {
success: {
viewTemplatePath: 'pages/admin/email-preview'
},
sendRawHtmlInstead: {
statusCode: 200,
outputType: 'string',
outputDescription: 'The raw HTML for the email as a string.',
},
},
fn: async function ({template, raw}) {
var path = require('path');
var url = require('url');
var util = require('util');
// Determine appropriate email layout and fake data to use.
let layout;
let fakeData;
switch (template) {
case 'internal/email-contact-form':
layout = false;
fakeData = {
contactName: 'Sage',
contactEmail: 'sage@example.com',
topic: 'Pricing question',
message: 'What is the difference between the "Free" plan and the "Premium" plan?',
};
break;
case 'email-reset-password':
layout = 'layout-email';
fakeData = {
token: '4-32fad81jdaf$329',
};
break;
case 'email-verify-account':
layout = 'layout-email';
fakeData = {
firstName: 'Fleet user',
token: '4-32fad81jdaf$329',
};
break;
case 'email-verify-new-email':
layout = 'layout-email';
fakeData = {
fullName: 'Fleet user',
token: '4-32fad81jdaf$329',
};
break;
case 'email-order-confirmation':
layout = 'layout-email';
fakeData = {
firstName: 'Fleet',
lastName: 'user',
};
break;
default:
layout = 'layout-email-newsletter';
fakeData = {
emailAddress: 'sage@example.com',
};
}
// Compile HTML template using the appropriate layout.
// > Note that we set the layout, provide access to core `url` package (for
// > building links and image srcs, etc.), and also provide access to core
// > `util` package (for dumping debug data in internal emails).
let emailTemplatePath = path.join('emails/', template);
if (layout) {
layout = path.relative(path.dirname(emailTemplatePath), path.resolve('layouts/', layout));
} else {
layout = false;
}
let sampleHtml = await sails.renderView(
emailTemplatePath,
Object.assign({layout, url, util, _ }, fakeData)
)
.intercept((err)=>{
err.message = 'Whoops, that email template failed to render. Could there be some fake data missing for this particular template in the `switch` statement api/controllers/admin/view-email-template-preview.js? Any chance you need to re-lift the app after making backend changes?\nMore details: '+err.message;
return err;
});
if (raw) {
// Respond with raw, rendered HTML for this email:
throw {sendRawHtmlInstead: sampleHtml};
} else {
// Respond with the previewer page for this email:
return {
sampleHtml,
template,
fakeData,
};
}
}
};

View File

@ -0,0 +1,60 @@
module.exports = {
friendlyName: 'View email templates',
description: 'Display "Email templates" page.',
exits: {
success: {
viewTemplatePath: 'pages/admin/email-templates'
}
},
fn: async function () {
var path = require('path');
// Sniff for top level email templates
let templatePaths = await sails.helpers.fs.ls.with({
dir: path.join(sails.config.paths.views, 'emails/'),
depth: 1,
includeDirs: false,
includeSymlinks: false,
});
let markdownEmailPaths = await sails.helpers.fs.ls.with({
dir: path.join(sails.config.paths.views, 'emails/newsletter'),
depth: 99,
includeDirs: false,
includeSymlinks: false,
});
markdownEmailPaths = markdownEmailPaths.map((templatePath)=>{
let relativePath = path.relative(path.join(sails.config.paths.views, 'emails/'), templatePath);
let extension = path.extname(relativePath);
return _.trimRight(relativePath, extension);
});
templatePaths = templatePaths.map((templatePath)=>{
let relativePath = path.relative(path.join(sails.config.paths.views, 'emails/'), templatePath);
let extension = path.extname(relativePath);
return _.trimRight(relativePath, extension);
});
// Respond with view.
return {
templatePaths,
markdownEmailPaths
};
}
};

View File

@ -0,0 +1,164 @@
module.exports = {
friendlyName: 'To HTML email',
description: 'Compile a Markdown string into an HTML string with styles added inline for the Fleet newsletter.',
extendedDescription:
'Expects GitHub-flavored Markdown syntax. Uses [`marked`](https://github.com/chjj/marked)@v0.3.5. '+
'Inspired by https://github.com/mikermcneil/machinepack-markdown/tree/5d8cee127e8ce45c702ec9bbb2b4f9bc4b7fafac',
moreInfoUrl: 'https://help.github.com/articles/basic-writing-and-formatting-syntax/',
sideEffects: 'cacheable',
inputs: {
mdString: {
description: 'Markdown string to convert',
example: '# hello world\n it\'s me, some markdown string \n\n ```js\n//but maybe i have code snippets too...\n```',
required: true
},
},
exits: {
success: {
outputFriendlyName: 'HTML',
outputExample: '<h1 id="hello-world">hello world</h1>\n<p> it&#39;s me, some markdown string </p>\n<pre><code class="lang-js">//but maybe i have code snippets too...</code></pre>\n'
},
unsafeMarkdown: {
friendlyName: 'Unsafe Markdown detected',
description: 'The provided input contained unsafe content (like HTML tags).'
}
},
fn: function(inputs, exits) {
const { marked } = require('marked');
var markedOpts = {
gfm: true,
tables: true,
breaks: false,
pedantic: false,
smartLists: true,
smartypants: false,
};
// Creating a custom renderer to add inline styles to HTML elements
let customRenderer = new marked.Renderer();
// For codeblocks
customRenderer.code = function(codeHTML) {
return '<pre style="padding: 24px; border: 1px solid #E2E4EA; overflow: auto; margin-bottom: 16px; margin-top: 16px; border-radius: 6px; background: #F9FAFC;"><code style="font-size: 13px; line-height: 16px; font-family: Source Code Pro;">'+_.escape(codeHTML)+'</code></pre>';
};
// For blockquotes
customRenderer.blockquote = function(quoteHTML) {
return `<blockquote>\n${quoteHTML}\n</blockquote>\n`;
};
customRenderer.heading = function(textHTML, level) {
let inlineStyles;
if(level === 1) { // For h1s
inlineStyles = 'font-weight: 800; font-size: 24px; line-height: 32px; margin-bottom: 16px;';
} else if (level === 2) { // For h2s
inlineStyles = 'font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;';
} else if (level === 3) { // for h3s
inlineStyles = 'font-weight: 700; font-size: 20px; line-height: 24px; margin-bottom: 16px;';
} else {// H4s or higher
inlineStyles = 'font-weight: 700; font-size: 16px; line-height: 20px; margin-bottom: 16px;';
}
return `<h${level} style="${inlineStyles}">\n${textHTML}\n</h${level}>\n`;
};
// For <hr> elements
customRenderer.hr = function() {
return `<hr style="border-top: 2px solid #E2E4EA; margin-top: 16px; margin-bottom: 16px; width: 100%;">`;
};
// For lists
customRenderer.list = function(bodyHTML, ordered) {
if(ordered){
return `<ol style="padding-left: 16px; margin-bottom: 32px;">\n${bodyHTML}</ol>\n`;
} else {
return `<ul style="padding-left: 16px; margin-bottom: 32px;">\n${bodyHTML}</ul>\n`;
}
};
// For list items
customRenderer.listitem = function(textHTML) {
return `<li style="margin-bottom: 16px;">\n${textHTML}\n</li>\n`;
};
customRenderer.paragraph = function(text) {
return `<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">\n${text}\n</p>\n`;
};
// For bold text
customRenderer.strong = function(textHTML) {
return `<strong style="display: inline; font-weight: 700; font-size: 16px; line-height: 24px;">${textHTML}</strong>`;
};
// For emphasized text
customRenderer.em = function(textHTML) {
return `<span style="display: inline; font-style: italic; font-size:16px;>${textHTML}</span>`;
};
// For inline codespans
customRenderer.codespan = function(codeHTML) {
return '<code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">'+codeHTML+'</code>';
};
// For links
customRenderer.link = function(href, title, textHTML) {
(href)=>{
let isExternal = ! href.match(/^https?:\/\/([^\.|blog]+\.)*fleetdm\.com/g);// « FUTURE: make this smarter with sails.config.baseUrl + _.escapeRegExp()
// Check if this link is to fleetdm.com or www.fleetdm.com.
let isBaseUrl = href.match(/^(https?:\/\/)([^\.]+\.)*fleetdm\.com$/g);
if (isExternal) {
href = href.replace(/(https?:\/\/([^"]+))/g, '$1 target="_blank"');
} else {
// Otherwise, change the link to be web root relative.
// (e.g. 'href="http://sailsjs.com/documentation/concepts"'' becomes simply 'href="/documentation/concepts"'')
// > Note: See the Git version history of "compile-markdown-content.js" in the sailsjs.com website repo for examples of ways this can work across versioned subdomains.
if (isBaseUrl) {
href = href.replace(/https?:\/\//, '');
} else {
href = href.replace(/https?:\/\//, '');
}
}
};
return `<a style="display: inline; color: #6A67FE; font-size: 16px; text-decoration: none; word-break: break-word;" href="${href}" target="_blank">${textHTML}</a>`;
};
// For images
customRenderer.image = function(href, title) {
let linkToImageInAssetsFolder = href.replace(/^(\.\.\/website\/assets)/gi, 'https://fleetdm.com');
return `<img style="max-width: 100%; margin-top: 40px; margin-bottom: 40px;" src="${linkToImageInAssetsFolder}" alt="${title}">`;
};
markedOpts.renderer = customRenderer;
// Now actually compile the markdown to HTML.
marked(inputs.mdString, markedOpts, function afterwards (err, htmlString) {
if (err) {
return exits.error(err);
}
return exits.success(htmlString);
});
}
};

View File

@ -0,0 +1,27 @@
parasails.registerPage('email-preview', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: {
//…
preview: 'Responsive',
},
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
//…
_.extend(this, SAILS_LOCALS);
},
mounted: async function() {
//…
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
//…
}
});

View File

@ -0,0 +1,25 @@
parasails.registerPage('email-templates', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: {
//…
},
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
//…
},
mounted: async function() {
//…
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
//…
}
});

View File

@ -61,6 +61,8 @@
@import 'pages/articles/basic-article.less';
@import 'pages/articles/articles.less';
@import 'pages/reports/state-of-device-management.less';
@import 'pages/admin/email-templates.less';
@import 'pages/admin/email-preview.less';
@import 'pages/osquery-table-details.less';
@import 'pages/admin/generate-license.less';

View File

@ -0,0 +1,76 @@
#email-preview {
padding-top: 80px;
padding-bottom: 80px;
overflow-x: scroll;
h2 {
font-size: 24px;
margin-bottom: 0px;
}
h3 {
font-size: 20px;
font-weight: 700;
margin-bottom: 0px;
}
[purpose='form-factors'] {
margin-top: 40px;
margin-bottom: 40px;
}
[purpose='preview-title'] {
max-width: 1200px;
margin-right: auto;
margin-left: auto;
}
[purpose='preview-selector'] {
cursor: pointer;
border: solid 1px @core-vibrant-blue-15;
min-width: 160px;
background-color: #FAFAFA;
height: 40px;
border-radius: 4px;
padding: 9px 16px 9px;
img {
margin-left: auto;
}
}
[purpose='preview-selector-dropwdown'] {
cursor: pointer;
}
.pretend-inbox-wrapper-for-sample-email {
margin-left: auto;
margin-right: auto;
.preview-iframe {
margin-top: 40px;
border: 1px solid #666;
width: 100%;
height: 900px;
max-width: 100%;
}
&.simulate-320px-width {
width: 320px;
}
&.simulate-480px-width {
width: 480px;
}
&.simulate-768px-width {
width: 768px;
}
&.simulate-1024px-width {
width: 1024px;
}
}
@media (max-width: 768px) {
padding-top: 40px;
h2 {
font-size: 20px;
}
h3 {
font-size: 18px;
}
}
}

View File

@ -0,0 +1,7 @@
#email-templates {
a {
color: @core-fleet-black;
}
}

View File

@ -208,6 +208,21 @@ module.exports.routes = {
},
},
'GET /admin/email-preview': {
action: 'admin/view-email-templates',
locals: {
layout: 'layouts/layout-customer'
},
},
'GET /admin/email-preview/*': {
action: 'admin/view-email-template-preview',
skipAssets: true,
locals: {
layout: 'layouts/layout-customer'
},
},
'GET /tables/:tableName': {
action: 'view-osquery-table-details',
},

View File

@ -0,0 +1,103 @@
module.exports = {
friendlyName: 'Generate HTML email from article',
description: 'Generate an HTML partial for the Fleet newsletter from Markdown files in the articles/ folder of the fleetdm/fleet repo.',
inputs: {
articleFilename: {type: 'string', description: 'The filename of the article that will be converted into an HTML email partial', required: true},
},
fn: async function ({ articleFilename }) {
let path = require('path');
let topLvlRepoPath = path.resolve(sails.config.appPath, '../');
let APP_PATH_TO_COMPILED_EMAIL_PARTIALS = 'views/emails/newsletter';
let extensionedArticleFilename = articleFilename;
// Since this script only handles Markdown files in the articles/ folders, we'll make the file extension optional.
if(!_.endsWith(articleFilename, '.md')) {
// If the file was specified without a file extension, we'll add `.md` to the provided filename, and log a warning.
extensionedArticleFilename = extensionedArticleFilename + '.md';
sails.log.warn('The filename provided is missing the .md file extension, appending `.md` to the provided articleFilename: '+articleFilename);
}
// Get the filename without the .md file extension. This will be used to build the final filename
let unextensionedArticleFilename = _.trimRight(extensionedArticleFilename, '.md');
// Build the filename for the final HTML partial.
let extensionedFileNameForEmailPartial = 'email-article-'+unextensionedArticleFilename.replace(/\./g, '-')+'.ejs';
// Find the Markdown file in the articles folder
let markdownFileToConvert = path.resolve(path.join(topLvlRepoPath, '/articles/'+extensionedArticleFilename));
if(!markdownFileToConvert) { // If we couldn't find the file specified, throw an error
throw new Error('Error: No Markdown file in found in the top level articles/ folder with the filename: '+articleFilename);
}
// If the file specified is not a markdown file, throw an error.
if (path.extname(markdownFileToConvert) !== '.md') {
throw new Error('Error: The specified file ('+articleFilename+') is not a valid Markdown file.'+markdownFileToConvert);
}
// Get the raw Markdown from the file.
let mdString = await sails.helpers.fs.read(markdownFileToConvert);
// Get the relative path of the Markdown file we are converting
let pageRelSourcePath = path.relative(path.join(topLvlRepoPath, 'articles/'), path.resolve(markdownFileToConvert));
// Remove any meta tags from the Markdown file before we convert it.
mdString = mdString.replace(/<meta[^>]*>/igm, '');
// Find and remove any iframe elements in the markdown file
for (let matchedIframe of (mdString.match(/<(iframe)[\s\S]+?<\/iframe>/igm) || [])) {
sails.log.warn('Removing an <iframe> element from the Markdown file before converting it into an HTML email: \n',matchedIframe);
mdString = mdString.replace(matchedIframe, '');
}
// Convert Markdown to HTML
let htmlEmailString = await sails.helpers.strings.toHtmlEmail(mdString);
// Modify path-relative links in the final HTML like `./…` and `../…` to make them absolute. (See https://github.com/fleetdm/fleet/issues/706#issuecomment-884641081 for more background)
htmlEmailString = htmlEmailString.replace(/(href="(\.\/[^"]+|\.\.\/[^"]+)")/g, (hrefString)=>{
let oldRelPath = hrefString.match(/href="(\.\/[^"]+|\.\.\/[^"]+)"/)[1];
let referencedPageSourcePath = path.resolve(path.join(topLvlRepoPath, 'articles/', pageRelSourcePath), '../', oldRelPath);
let possibleReferencedUrlHash = oldRelPath.match(/(\.md#)([^/]*$)/) ? oldRelPath.match(/(\.md#)([^/]*$)/)[2] : false;
let referencedPageNewUrl = 'https://fleetdm.com/' +
(
(path.relative(topLvlRepoPath, referencedPageSourcePath).replace(/(^|\/)([^/]+)\.[^/]*$/, '$1$2').split(/\//).map((fileOrFolderName) => fileOrFolderName.toLowerCase()).join('/'))
.split(/\//)
.map((fileOrFolderName) => encodeURIComponent(fileOrFolderName.replace(/^[0-9]+[\-]+/,''))).join('/')
).replace(/\/?readme\.?m?d?$/i, '');
if(possibleReferencedUrlHash) {
referencedPageNewUrl = referencedPageNewUrl + '#' + encodeURIComponent(possibleReferencedUrlHash);
}
return `href="${referencedPageNewUrl}"`;
});
// Find the location where this file will be saved.
let htmlEmailOutputPath = path.resolve(sails.config.appPath, path.join(APP_PATH_TO_COMPILED_EMAIL_PARTIALS, extensionedFileNameForEmailPartial));
// Delete existing HTML output from previous runs, if any exists.
await sails.helpers.fs.rmrf(htmlEmailOutputPath);
sails.log('Generated HTML partial from a Markdown article at: '+htmlEmailOutputPath);
// Save the HTML output in website/pages/emails/newsletter-partials
await sails.helpers.fs.write(htmlEmailOutputPath, htmlEmailString);
}
};

View File

@ -1,9 +1,9 @@
<% /* Note: This is injected into `views/layouts/layout-email.ejs` */ %>
<p style="margin-bottom: 25px;">Dear <%= fullName %>,</p>
<p style="margin-bottom: 25px;">You recently requested an update to the email address for your account. To verify your new email, please click the button below:</p>
<div style="margin-bottom: 25px; text-align: center;">
<a style="background-color: #28AFB0; display: inline-block; font-size: 1.1em; color: #fff; padding: 10px 35px 10px; font-weight: 500; text-decoration: none; border-radius: 7px;" href="<%= url.resolve(sails.config.custom.baseUrl,'/email/confirm')+'?token='+encodeURIComponent(token) %>">Confirm email</a>
<div style="margin-bottom: 25px; text-align: left;">
<a style="background-color: #6A67FE; display: inline-block; font-size: 1.1em; color: #fff; padding: 10px 35px 10px; font-weight: 500; text-decoration: none; border-radius: 7px;" href="<%= url.resolve(sails.config.custom.baseUrl,'/email/confirm')+'?token='+encodeURIComponent(token) %>">Confirm email</a>
</div>
<p style="margin-bottom: 25px;">If you have any trouble, try pasting this link in your browser: <a style="color: #28AFB0; word-wrap: break-word;" href="<%= url.resolve(sails.config.custom.baseUrl,'/email/confirm')+'?token='+encodeURIComponent(token) %>"><%= url.resolve(sails.config.custom.baseUrl,'/email/confirm')+'?token='+encodeURIComponent(token) %></a></p>
<p style="margin-bottom: 25px;">If you have any trouble, try pasting this link in your browser: <a style="color: #6A67FE; word-wrap: break-word;" href="<%= url.resolve(sails.config.custom.baseUrl,'/email/confirm')+'?token='+encodeURIComponent(token) %>"><%= url.resolve(sails.config.custom.baseUrl,'/email/confirm')+'?token='+encodeURIComponent(token) %></a></p>
<p style="margin-bottom: 5px;">Sincerely,</p>
<p style="margin-top: 0px;">The NEW_APP_NAME Team</p>
<p style="margin-top: 0px;">The Fleet Team</p>

View File

@ -0,0 +1,216 @@
<h1 style="font-weight: 800; font-size: 24px; line-height: 32px; margin-bottom: 16px;">
Fleet 4.22.0 | Easier access to host information, better query console UX, and clearer display names
</h1>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<img style="max-width: 100%; margin-top: 40px; margin-bottom: 40px;" src="https://fleetdm.com/images/articles/fleet-4.22.0-cover-800x450@2x.jpg" alt="null">
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Fleet 4.22.0 is up and running. Check out the full <a style="display: inline; color: #6A67FE; font-size: 16px; text-decoration: none; word-break: break-word;" href="https://github.com/fleetdm/fleet/releases/tag/fleet-v4.22.0" target="_blank">changelog</a> or continue reading to get the highlights.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
For upgrade instructions, see our <a style="display: inline; color: #6A67FE; font-size: 16px; text-decoration: none; word-break: break-word;" href="https://fleetdm.com/docs/deploying/upgrading-fleet" target="_blank">upgrade guide</a> in the Fleet docs.
</p>
<h2 style="font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;">
Highlights
</h2>
<ul style="padding-left: 16px; margin-bottom: 32px;">
<li style="margin-bottom: 16px;">
Identify missing hosts easily.
</li>
<li style="margin-bottom: 16px;">
Know which hosts have low disk space.
</li>
<li style="margin-bottom: 16px;">
Enjoy better UX in the query console.
</li>
<li style="margin-bottom: 16px;">
See clearer host display names.
</li>
</ul>
<h2 style="font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;">
Identify missing hosts easily
</h2>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<strong style="display: inline; font-weight: 700; font-size: 16px; line-height: 24px;">Available in Fleet Premium</strong>
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<img style="max-width: 100%; margin-top: 40px; margin-bottom: 40px;" src="https://fleetdm.com/images/articles/fleet-4.22.0-missing-hosts-1-800x450@2x.jpg" alt="null">
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Discovering that hosts are missing can cause any admin or engineer to worry. Did someone take parental leave? Is their laptop broken? Or was their computer stolen?
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Before you can answer these questions, you need to know which hosts are missing. Fleet makes your search simple. Premium users can see how many hosts have been offline for 30 days or more on the Fleet UI homepage.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Clicking the Missing card will take you to the Hosts page with the Missing filter applied — generating a list of all your missing hosts. You can also navigate to this list by pasting this URL into your browser: <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">fleet.organization.com/hosts/manage/?status=missing</code>.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<img style="max-width: 100%; margin-top: 40px; margin-bottom: 40px;" src="https://fleetdm.com/images/articles/fleet-4.22.0-missing-hosts-2-800x450@2x.jpg" alt="null">
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
This list shows you details about your missing hosts, including the name, team, and when its host vitals were last fetched.
</p>
<h2 style="font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;">
Know which hosts have low disk space
</h2>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<strong style="display: inline; font-weight: 700; font-size: 16px; line-height: 24px;">Available in Fleet Premium</strong>
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<img style="max-width: 100%; margin-top: 40px; margin-bottom: 40px;" src="https://fleetdm.com/images/articles/fleet-4.22.0-low-disk-space-1-800x450@2x.jpg" alt="null">
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Making sure hosts have the latest operating system updates is tough enough. End users have their own priorities and computer maintenance is usually low on the list. Deadlines get even tighter when IT admins find out hosts dont have enough disk space. Fleet can help you avoid these unpleasant surprises.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
On the Fleet UI homepage, Premium users can see how many hosts have less than 32 GB of disk space remaining. Clicking the Low disk space card generates a list of all these devices. You can also get to this list by pasting this URL into your browser: <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">fleet.organization.com/hosts/manage/?low_disk_space=32</code>.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<img style="max-width: 100%; margin-top: 40px; margin-bottom: 40px;" src="https://fleetdm.com/images/articles/fleet-4.22.0-low-disk-space-2-800x450@2x.jpg" alt="null">
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
This list lets you see exactly how much disk space remains on each device. It also provides identifying information about the devices, like the hostname and team. So, you can prioritize devices and contact users accordingly.
</p>
<h2 style="font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;">
Enjoy better UX in the query console
</h2>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<strong style="display: inline; font-weight: 700; font-size: 16px; line-height: 24px;">Available in Fleet Free and Fleet Premium</strong>
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
You can find so much data with Fleet. Take a look at our <a style="display: inline; color: #6A67FE; font-size: 16px; text-decoration: none; word-break: break-word;" href="https://fleetdm.com/queries" target="_blank">query library</a> to see what we mean. But, like any new tool, all the possibilities can seem overwhelming. First-time Fleet users will now enjoy a more welcoming experience with our updated query console panel.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Have you heard of osquery evented tables? This configuration helps you view data over time — without repeatedly refreshing queries yourself. Learn more by reading our guide on <a style="display: inline; color: #6A67FE; font-size: 16px; text-decoration: none; word-break: break-word;" href="https://fleetdm.com/guides/osquery-evented-tables-overview" target="_blank">how to use osquery evented tables</a>. The Evented Table tag lets you know right away if the query youre running is evented.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Compatible operating systems display in the same order throughout the Fleet UI, making results even easier to check at a glance.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Weve made a couple improvements to the Columns section. Columns required in the WHERE clause are listed first with an asterisk. Otherwise, columns are listed alphabetically.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Certain tables may include a column with a unique qualification, like being required in the WHERE clause. A column could also be platform-specific (even if its compatible overall) or default to the root. In these instances, youll see bold notes at the bottom of the column tooltip.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Finally, we saved you some scrolling by wrapping SQL examples.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<img style="max-width: 100%; margin-top: 40px; margin-bottom: 40px;" src="https://fleetdm.com//images/articles/fleet-4.22.0-updated-query-console-sidebar-800x570@2x.jpg" alt="null">
</p>
<h2 style="font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;">
See clearer host display names
</h2>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
<strong style="display: inline; font-weight: 700; font-size: 16px; line-height: 24px;">Available in Fleet Free and Fleet Premium</strong>
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Were familiar with the pros and cons of remote work by now. (Insert joke about pajamas, pets, and/or neighbors here.) But theres one issue in particular that Fleet now solves: confusing hostnames.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
When working from home, a devices hostname can be whatever the internet service provider (ISP) assigns to the device. For example, a hostname could be <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">aianas-mbp.cable.rcn.com</code>. That isnt very helpful, especially if you have hundreds of similar results.
</p>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Fleet 4.22.0 no longer defaults to whatever the ISP provides. Instead, Fleet uses the computer name or the hostname for the display name. If the computer name has been set, then that will be the display name. If not, then Fleet will use the hostname.
</p>
<h2 style="font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;">
More new features, improvements, and bug fixes
</h2>
<ul style="padding-left: 16px; margin-bottom: 32px;">
<li style="margin-bottom: 16px;">
Added functionality to consider device tokens as expired after one hour. This change is not compatible with older versions of Fleet Desktop. We recommend to manually update Orbit and Fleet Desktop to &gt; v1.0.0 in addition to upgrading the server if:<ul style="padding-left: 16px; margin-bottom: 32px;">
<li style="margin-bottom: 16px;">
You&#39;re managing your own TUF server.
</li>
<li style="margin-bottom: 16px;">
You have auto-updates disabled (<code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">fleetctl package [...] --disable-updates</code>).
</li>
<li style="margin-bottom: 16px;">
You have channels pinned to an older version (<code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">fleetctl package [...] --orbit-channel 1.0.0 --desktop-channel 1.1.0</code>).
</li>
</ul>
</li>
<li style="margin-bottom: 16px;">
Added security headers to HTML, CSV, and installer responses.
</li>
<li style="margin-bottom: 16px;">
Added validation of the <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">command_line_flags</code> object in the Agent Options section of Organization Settings and Team Settings.
</li>
<li style="margin-bottom: 16px;">
Added logic to clean up irrelevant policies for a host on re-enrollment (e.g., if a host changes its OS from linux to macOS or it changes teams).
</li>
<li style="margin-bottom: 16px;">
Added the <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">inherited_policies</code> array to the <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">GET /teams/{team_id}/policies</code> endpoint that lists the global policies inherited by the team, along with the pass/fail counts for the hosts on that team.
</li>
<li style="margin-bottom: 16px;">
Added a new UI state for when results are coming in from a live query or policy query.
</li>
<li style="margin-bottom: 16px;">
Added better team name suggestions to the Create teams modal.
</li>
<li style="margin-bottom: 16px;">
Added usage statistics for the weekly count of aggregate policy violation days. One policy violation day is counted for each policy that a host is failing, measured as of the time the count increments. The count increments once per 24-hour interval and resets each week.
</li>
<li style="margin-bottom: 16px;">
Clarified last seen time and last fetched time in the Fleet UI.
</li>
<li style="margin-bottom: 16px;">
Translated technical error messages returned by Agent options validation to be more user-friendly.
</li>
<li style="margin-bottom: 16px;">
Renamed machine serial to serial number and IPv4 properly to private IP address.
</li>
<li style="margin-bottom: 16px;">
Fleet Premium: Updated Fleet Desktop to use the <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">/device/{token}/desktop</code> API route to display the number of failing policies.
</li>
<li style="margin-bottom: 16px;">
Made host details software tables more responsive by adding links to software details.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug in which a user would not be rerouted to the Home page if already logged in.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug in which clicking the select all checkbox did not select all in some cases.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug introduced in 4.21.0 where a Windows-specific query was being sent to non-Windows hosts, causing an error in query ingestion for <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">directIngestOSWindows</code>.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug in which uninstalled software (DEB packages) appeared in Fleet.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug in which a team that didn&#39;t have <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">config.features</code> settings was edited via the UI, then both <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">features.enable_host_users</code> and <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">features.enable_software_inventory</code> would be false insead of the global default.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug that resulted in false negatives for vulnerable versions of Zoom, Google Chrome, Adobe Photoshop, Node.js, Visual Studio Code, Adobe Media Encoder, VirtualBox, Adobe Premiere Pro, Pip, and Firefox software.
</li>
<li style="margin-bottom: 16px;">
Fixed bug that caused duplicated vulnerabilities to be sent to third-party integrations.
</li>
<li style="margin-bottom: 16px;">
Fixed panic in <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">ingestKubequeryInfo</code> query ingestion.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug in which host_count and user_count returned as <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">0</code> in the <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">teams/{id}</code> endpoint.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug in which tooltips for Munki issues would be cut off at the edge of the browser window.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug in which running <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">fleetctl apply</code> with the <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">--dry-run</code> flag would fail in some cases.
</li>
<li style="margin-bottom: 16px;">
Fixed a bug in which the Hosts table displayed 20 hosts per page.
</li>
<li style="margin-bottom: 16px;">
Fixed a server panic that occured when a team was edited via YAML without an <code style="display: inline; background: #F1F0FF; color: #192147; padding: 4px 8px; font-size: 13px; line-height: 16px; font-family: Courier New;">agent_options</code> key.
</li>
</ul>
<h2 style="font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;">
Ready to upgrade?
</h2>
<p style="font-size: 16px; line-height: 24px; font-weight: 400; margin-bottom: 16px;">
Visit our <a style="display: inline; color: #6A67FE; font-size: 16px; text-decoration: none; word-break: break-word;" href="https://fleetdm.com/docs/deploying/upgrading-fleet" target="_blank">Upgrade guide</a> in the Fleet docs for instructions on updating to Fleet 4.22.0.
</p>

View File

@ -198,6 +198,8 @@
<script src="/js/pages/account/account-overview.page.js"></script>
<script src="/js/pages/account/edit-password.page.js"></script>
<script src="/js/pages/account/edit-profile.page.js"></script>
<script src="/js/pages/admin/email-preview.page.js"></script>
<script src="/js/pages/admin/email-templates.page.js"></script>
<script src="/js/pages/admin/generate-license.page.js"></script>
<script src="/js/pages/articles/articles.page.js"></script>
<script src="/js/pages/articles/basic-article.page.js"></script>

View File

@ -1,11 +1,11 @@
<% /* Default layout for email templates */ %>
<div style="width: 100%; font-family: 'Nunito Sans', sans-serif; box-sizing: border-box; padding-bottom: 25px; margin: 0;">
<div style="color: #000; font-size: 16px; box-sizing: border-box; padding: 25px; width: 100%; max-width: 600px; margin-left: auto; margin-right: auto;">
<div style="background-color: #F9FAFC; width: 100%; font-family: 'Nunito Sans','Helvetica','arial', sans-serif; box-sizing: border-box; padding: 8px; margin: 0; word-break: break-word;">
<div style="border: 1px #e3e3e3 solid; background-color: #FFF ;color: #192147; font-size: 16px; box-sizing: border-box; width: 100%; max-width: 900px; margin-left: auto; margin-right: auto; padding: 24px;">
<div style="background: transparent; text-align: left;">
<a href="https://fleetdm.com"><img style="display: inline-block; width: 162px; height: 92px; width: auto;" alt="Logo" src="https://fleetdm.com/images/logo-blue-162x92@2x.png"/></a>
</div>
<%- body %>
<hr style="color: #E2E4EA;"/>
<hr style="margin-top: 40px; color: #E2E4EA;"/>
<div style="display: inline-flex; padding-top: 32px;">
<a href="https://fleetdm.com"><img style="height: 20px; width: 20px; margin-right: 24px;" alt="Fleet logo" src="<%= url.resolve(sails.config.custom.baseUrl,'/images/logo-fleet-20x20@2x.png')%>"></a>
<a href="https://twitter.com/fleetctl"><img style="height: 20px; width: 24px; margin-right: 24px;" alt="Follow Fleet on Twitter" src="<%= url.resolve(sails.config.custom.baseUrl,'images/logo-twitter-50x44@2x.png')%>"></a>
@ -15,7 +15,7 @@
<p>© 2022 Fleet Device Management Inc. All trademarks, service marks, and company names are the property of their respective owners.</p>
</div>
<div>
You recieved this email because youre subscribed to the Fleet newsletter. <a href="https://fleetdm.com/unsubscribe?emailAddress=<%= encodeURIComponent(emailAddress) %>">Unsubscribe</a>
<p style="font-size: 12px; color: #3E4771;">You recieved this email because youre subscribed to the Fleet newsletter. <a href="<%= url.resolve(sails.config.custom.baseUrl, '/unsubscribe?emailAddress='+encodeURIComponent(emailAddress)) %>">Unsubscribe</a></p>
</div>
</div>
</div>

View File

@ -205,6 +205,8 @@
<script src="/js/pages/account/account-overview.page.js"></script>
<script src="/js/pages/account/edit-password.page.js"></script>
<script src="/js/pages/account/edit-profile.page.js"></script>
<script src="/js/pages/admin/email-preview.page.js"></script>
<script src="/js/pages/admin/email-templates.page.js"></script>
<script src="/js/pages/admin/generate-license.page.js"></script>
<script src="/js/pages/articles/articles.page.js"></script>
<script src="/js/pages/articles/basic-article.page.js"></script>

View File

@ -306,6 +306,8 @@
<script src="/js/pages/account/account-overview.page.js"></script>
<script src="/js/pages/account/edit-password.page.js"></script>
<script src="/js/pages/account/edit-profile.page.js"></script>
<script src="/js/pages/admin/email-preview.page.js"></script>
<script src="/js/pages/admin/email-templates.page.js"></script>
<script src="/js/pages/admin/generate-license.page.js"></script>
<script src="/js/pages/articles/articles.page.js"></script>
<script src="/js/pages/articles/basic-article.page.js"></script>

View File

@ -404,6 +404,8 @@
<script src="/js/pages/account/account-overview.page.js"></script>
<script src="/js/pages/account/edit-password.page.js"></script>
<script src="/js/pages/account/edit-profile.page.js"></script>
<script src="/js/pages/admin/email-preview.page.js"></script>
<script src="/js/pages/admin/email-templates.page.js"></script>
<script src="/js/pages/admin/generate-license.page.js"></script>
<script src="/js/pages/articles/articles.page.js"></script>
<script src="/js/pages/articles/basic-article.page.js"></script>

View File

@ -0,0 +1,82 @@
<div id="email-preview" v-cloak>
<div class="container flex-column d-flex align-items-start">
<h2>Previewing {{template}}</h2>
<div purpose="form-factors" class="d-flex flex-sm-row flex-column align-items-sm-center align-items-start">
<p class="mr-3 mb-0">Form factor</p>
<div purpose="preview-selector" class="mt-2 mt-sm-0 d-flex flex-row justify-content-between align-items-center"
id="dropdownMenuSelectPurpose" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{preview}} <img class="d-flex ml-3" style="width: 16px; height: 16px;" src="/images/chevron-down-16x16@2x.png" alt="a small shevron pointing downwards">
</div>
<div purpose="preview-selector-dropwdown" class="dropdown-menu" aria-labelledby="dropdownMenuSelectPurpose">
<div class="dropdown-item d-block"
@click="preview = 'Responsive'">Responsive</div>
<div class="dropdown-item d-block"
@click="preview = 'Mobile (portrait)'">Mobile (portrait)</div>
<div class="dropdown-item d-block"
@click="preview = 'Mobile (landscape)'">Mobile (landscape)</div>
<div class="dropdown-item d-block"
@click="preview = 'Tablet (portait)'">Tablet (portrait)</div>
<div class="dropdown-item d-block"
@click="preview = 'Tablet (landscape)'">Tablet (landscape)</div>
</div>
</div>
</div>
<div v-if="preview === 'Mobile (portrait)'">
<div class="container">
<h3>Mobile (320px width)</h3>
</div>
<div class="simulate-320px-width pretend-inbox-wrapper-for-sample-email">
<iframe class="preview-iframe" frameborder="0" border="0" cellspacing="0" :src="'/admin/email-preview/'+template+'?raw=true'">
<%- sampleHtml %>
</iframe>
</div>
</div>
<div v-if="preview === 'Mobile (landscape)'">
<div class="container">
<h3>Mobile (480px width)</h3>
</div>
<div class="simulate-480px-width pretend-inbox-wrapper-for-sample-email" >
<iframe class="preview-iframe" frameborder="0" border="0" cellspacing="0" :src="'/admin/email-preview/'+template+'?raw=true'">
<%- sampleHtml %>
</iframe>
</div>
</div>
<div v-if="preview === 'Tablet (portait)'">
<div class="container">
<h3>Tablet (768px width)</h3>
</div>
<div class="simulate-768px-width pretend-inbox-wrapper-for-sample-email" >
<iframe class="preview-iframe" frameborder="0" border="0" cellspacing="0" :src="'/admin/email-preview/'+template+'?raw=true'">
<%- sampleHtml %>
</iframe>
</div>
</div>
<div v-if="preview === 'Tablet (landscape)'">
<div class="container">
<h3>Tablet (1024px width)</h3>
</div>
<div class="simulate-1024px-width pretend-inbox-wrapper-for-sample-email" >
<iframe class="preview-iframe" frameborder="0" border="0" cellspacing="0" :src="'/admin/email-preview/'+template+'?raw=true'">
<%- sampleHtml %>
</iframe>
</div>
</div>
<div class="w-100" v-if="preview === 'Responsive'">
<div class="container">
<h3 class="mb-0">Responsive:</h3>
<p>Resize the browser to preview different widths</p>
</div>
<div class="pretend-inbox-wrapper-for-sample-email w-100 px-3 px-lg-5">
<iframe class="preview-iframe" frameborder="0" border="0" cellspacing="0" :src="'/admin/email-preview/'+template+'?raw=true'">
<%- sampleHtml %>
</iframe>
</div>
</div>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>

View File

@ -0,0 +1,19 @@
<div id="email-templates" v-cloak>
<div class="container pt-5">
<h2>Preview email templates</h2>
<ul>
<li v-for="templatePath of templatePaths">
<a :href="'/admin/email-preview/'+templatePath">{{templatePath}}</a>
</li>
</ul>
<div v-if="markdownEmailPaths.length !== 0">
<h2>Preview emails generated from Markdown content</h2>
<ul>
<li v-for="templatePath of markdownEmailPaths">
<a :href="'/admin/email-preview/'+templatePath">{{templatePath}}</a>
</li>
</ul>
</div>
</div>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>