-
Notifications
You must be signed in to change notification settings - Fork 2
TRIVIR-2320 | Extract HTML and CSS from Themes and Email Templates #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bb7786a
64c3842
dff3212
4cb7989
340a6cd
022a3b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,6 @@ import { Option } from 'commander'; | |
|
|
||
| import { getTokens } from '../../ops/AuthenticateOps'; | ||
| import { | ||
| exportEmailTemplatesToFile, | ||
| exportEmailTemplatesToFiles, | ||
| exportEmailTemplateToFile, | ||
| } from '../../ops/EmailTemplateOps'; | ||
|
|
@@ -57,6 +56,12 @@ export default function setup() { | |
| 'Does not include metadata in the export file.' | ||
| ) | ||
| ) | ||
| .addOption( | ||
| new Option( | ||
| '-x, --no-extract', | ||
| 'Do not extract HTML and CSS to a separate file' | ||
| ) | ||
| ) | ||
| .action( | ||
| // implement command logic inside action handler | ||
| async (host, realm, user, password, options, command) => { | ||
|
|
@@ -79,33 +84,29 @@ export default function setup() { | |
| }" from realm "${state.getRealm()}"...` | ||
| ); | ||
| const outcome = await exportEmailTemplateToFile( | ||
| options.extract, | ||
| options.templateId, | ||
| options.file, | ||
| options.metadata | ||
|
Comment on lines
+87
to
90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sure to update the ordering here based on my other comments |
||
| ); | ||
| if (!outcome) process.exitCode = 1; | ||
| } | ||
| // --all -a | ||
| // --all-separate -A | ||
| else if ( | ||
| options.all && | ||
| (options.all || options.allSeparate) && | ||
| (await getTokens(false, true, deploymentTypes)) | ||
| ) { | ||
| verboseMessage('Exporting all email templates to a single file...'); | ||
| const outcome = await exportEmailTemplatesToFile( | ||
| const message = options.all ? "single" : "separate"; | ||
| verboseMessage(`Exporting all email templates to a ${message} file...`); | ||
| const outcome = await exportEmailTemplatesToFiles( | ||
| options.all ? "all" : "separate", | ||
| options.extract, | ||
| options.file, | ||
| options.metadata | ||
| ); | ||
| if (!outcome) process.exitCode = 1; | ||
| } | ||
| // --all-separate -A | ||
| else if ( | ||
| options.allSeparate && | ||
| (await getTokens(false, true, deploymentTypes)) | ||
| ) { | ||
| verboseMessage('Exporting all email templates to separate files...'); | ||
| const outcome = await exportEmailTemplatesToFiles(options.metadata); | ||
| if (!outcome) process.exitCode = 1; | ||
| } | ||
| // unrecognized combination of options or no options | ||
| else { | ||
| printMessage( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,10 +17,12 @@ import { | |
| } from '../utils/Console'; | ||
| import { cloneDeep } from './utils/OpsUtils'; | ||
| import wordwrap from './utils/Wordwrap'; | ||
| import { ExportMetaData } from '@rockcarver/frodo-lib/types/ops/OpsTypes'; | ||
|
|
||
| const { | ||
| validateImport, | ||
| getTypedFilename, | ||
| saveTextToFile, | ||
| saveJsonToFile, | ||
| getFilePath, | ||
| getWorkingDirectory, | ||
|
|
@@ -40,10 +42,16 @@ const EMAIL_TEMPLATE_FILE_TYPE = 'template.email'; | |
| const regexEmailTemplateType = new RegExp(`${EMAIL_TEMPLATE_TYPE}/`, 'g'); | ||
|
|
||
| // use a function vs a template variable to avoid problems in loops | ||
| function getFileDataTemplate() { | ||
| function getFileDataTemplate(): Record<string, EmailTemplateSkeleton | ExportMetaData> { | ||
| return { | ||
| meta: {}, | ||
| emailTemplate: {}, | ||
| emailTemplate: {} as EmailTemplateSkeleton, | ||
| }; | ||
| } | ||
|
|
||
| function getFileDataHTMLTemplate() { | ||
| return { | ||
| html: {}, | ||
| css: {}, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -159,6 +167,85 @@ export async function listEmailTemplates( | |
| return true; | ||
| } | ||
|
|
||
| function removeHtmlAndCssFromJson( | ||
| emailTemplate: EmailTemplateSkeleton, | ||
| isDefault: boolean, | ||
| extract: boolean = false | ||
| ) { | ||
| if (extract) { | ||
| emailTemplate.html = {}; | ||
| if (emailTemplate.message) delete emailTemplate.message; | ||
| if (emailTemplate.styles) delete emailTemplate.styles; | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (isDefault) { | ||
| if (emailTemplate.message) delete emailTemplate.message; | ||
| } else { | ||
| emailTemplate.html = {}; | ||
| if (emailTemplate.styles) delete emailTemplate.styles; | ||
| } | ||
| } | ||
|
|
||
| export function extractHtmlAndCssToFiles( | ||
| templateId: string, | ||
| fileName: string, | ||
| templateData: EmailTemplateSkeleton, | ||
| fileData?: Record<string, EmailTemplateSkeleton | ExportMetaData>, | ||
| extract: boolean = false, | ||
| includeMeta: boolean = false | ||
| ): Record<string, EmailTemplateSkeleton | ExportMetaData> | undefined { | ||
| let singleFileData: Record<string, EmailTemplateSkeleton | ExportMetaData> | undefined; | ||
|
|
||
| if (!fileData) { | ||
| singleFileData = getFileDataTemplate(); | ||
| } | ||
|
|
||
| const formattedTemplate = structuredClone(templateData); | ||
|
|
||
| const isDefault = templateData.html && !templateData.advancedEditor; | ||
| const htmlReference = isDefault ? templateData.html : templateData.message; | ||
|
|
||
| if (extract) { | ||
| let fullFilePath = ""; | ||
|
|
||
| Object.entries(htmlReference).forEach(([key, value]) => { | ||
| const htmlFileName = fileName + key + ".html"; | ||
| fullFilePath = getFilePath(htmlFileName, true); | ||
|
|
||
| saveTextToFile(value, fullFilePath); | ||
| }); | ||
|
|
||
| if (isDefault && templateData.styles && templateData.styles !== "") { | ||
| const stylesFileName = fileName + "css"; | ||
| const fullFilePath = getFilePath(stylesFileName, true); | ||
|
|
||
| saveTextToFile(templateData.styles as string, fullFilePath); | ||
| } | ||
| } | ||
|
|
||
| removeHtmlAndCssFromJson(formattedTemplate, isDefault, extract); | ||
|
|
||
| (fileData ?? singleFileData).emailTemplate[templateId] = formattedTemplate; | ||
|
|
||
| // If the HTML prop exists at all, it'll always be an advanced editor template | ||
| // regardless of what the advanced editor is set to. So, set this to true anyways. | ||
| if (!isDefault) { | ||
| (fileData ?? singleFileData).emailTemplate[templateId].advancedEditor = true; | ||
| } | ||
|
|
||
| if (singleFileData) { | ||
| saveJsonToFile(singleFileData, getFilePath(fileName + "json", true), includeMeta); | ||
| } | ||
|
|
||
| return fileData; | ||
| } | ||
|
|
||
| export function getEmailTemplateExportFromFile() { | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * Export single email template to a file | ||
| * @param {string} templateId email template id to export | ||
|
|
@@ -167,6 +254,7 @@ export async function listEmailTemplates( | |
| * @return {Promise<boolean>} a promise that resolves to true if successful, false otherwise | ||
| */ | ||
| export async function exportEmailTemplateToFile( | ||
| extract: boolean = false, | ||
| templateId: string, | ||
| file: string, | ||
| includeMeta: boolean = true | ||
|
Comment on lines
+257
to
260
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For extract, I would change the order of these parameters to be as follows: templateId: string,
file: string,
extract: boolean = false,
includeMeta: boolean = trueThe reason being that extract is optional because you give it a default value, therefore it should show up after all required parameters. |
||
|
|
@@ -175,19 +263,29 @@ export async function exportEmailTemplateToFile( | |
| try { | ||
| let fileName = file; | ||
| if (!fileName) { | ||
| fileName = getTypedFilename(templateId, EMAIL_TEMPLATE_FILE_TYPE); | ||
| fileName = getTypedFilename(templateId, EMAIL_TEMPLATE_FILE_TYPE).split("json")[0]; | ||
| } | ||
| const filePath = getFilePath(fileName, true); | ||
| const filePath = getFilePath(fileName + "json", true); | ||
|
|
||
| indicatorId = createProgressIndicator( | ||
| 'determinate', | ||
| 1, | ||
| `Exporting ${templateId}` | ||
| ); | ||
| const templateData = await readEmailTemplate(templateId); | ||
|
|
||
| updateProgressIndicator(indicatorId, `Writing file ${filePath}`); | ||
| const fileData = getFileDataTemplate(); | ||
| fileData.emailTemplate[templateId] = templateData; | ||
| saveJsonToFile(fileData, filePath, includeMeta); | ||
|
|
||
| const templateData = await readEmailTemplate(templateId); | ||
|
|
||
| extractHtmlAndCssToFiles( | ||
| templateId, | ||
| fileName, | ||
| templateData, | ||
| undefined, | ||
| extract, | ||
| includeMeta | ||
| ); | ||
|
|
||
| stopProgressIndicator( | ||
| indicatorId, | ||
| `Exported ${templateId['brightCyan']} to ${filePath['brightCyan']}.` | ||
|
|
@@ -201,13 +299,15 @@ export async function exportEmailTemplateToFile( | |
| } | ||
|
|
||
| /** | ||
| * Export all email templates to file | ||
| * Export all email templates to file(s) | ||
| * @param {string} file optional filename | ||
| * @param {boolean} includeMeta true to include metadata, false otherwise. Default: true | ||
| * @return {Promise<boolean>} a promise that resolves to true if successful, false otherwise | ||
| */ | ||
| export async function exportEmailTemplatesToFile( | ||
| file: string, | ||
| export async function exportEmailTemplatesToFiles( | ||
| type: string, | ||
| extract: boolean = false, | ||
| file?: string, | ||
| includeMeta: boolean = true | ||
|
Comment on lines
+309
to
311
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-order this similar to my other comment. Specifically, but extract after file since file is a required parameter |
||
| ): Promise<boolean> { | ||
| try { | ||
|
|
@@ -218,48 +318,39 @@ export async function exportEmailTemplatesToFile( | |
| EMAIL_TEMPLATE_FILE_TYPE | ||
| ); | ||
| } | ||
| const filePath = getFilePath(fileName, true); | ||
|
|
||
| const exportData = await exportEmailTemplates(true); | ||
| saveJsonToFile(exportData, filePath, includeMeta); | ||
| return true; | ||
| } catch (error) { | ||
| printError(error); | ||
| } | ||
| return false; | ||
| } | ||
| let fileData: Record<string, EmailTemplateSkeleton | ExportMetaData> | undefined; | ||
|
|
||
| /** | ||
| * Export all email templates to separate files | ||
| * @param {boolean} includeMeta true to include metadata, false otherwise. Default: true | ||
| * @return {Promise<boolean>} a promise that resolves to true if successful, false otherwise | ||
| */ | ||
| export async function exportEmailTemplatesToFiles( | ||
| includeMeta: boolean = true | ||
| ): Promise<boolean> { | ||
| let indicatorId; | ||
| try { | ||
| const exportData = Object.entries( | ||
| (await exportEmailTemplates(true)).emailTemplate | ||
| ); | ||
| indicatorId = createProgressIndicator( | ||
| 'determinate', | ||
| exportData.length, | ||
| 'Writing email templates' | ||
| ); | ||
| for (const [templateId, template] of exportData) { | ||
| const fileName = getTypedFilename(templateId, EMAIL_TEMPLATE_FILE_TYPE); | ||
| const fileData = getFileDataTemplate(); | ||
| updateProgressIndicator(indicatorId, `Exporting ${templateId}`); | ||
| fileData.emailTemplate[templateId] = template; | ||
| saveJsonToFile(fileData, getFilePath(fileName, true), includeMeta); | ||
| if (type === "all") { | ||
| fileData = getFileDataTemplate(); | ||
|
|
||
| if (!extract && includeMeta) { | ||
| fileData["meta"] = exportData.meta; | ||
| } | ||
| } | ||
| stopProgressIndicator( | ||
| indicatorId, | ||
| `${exportData.length} templates written.` | ||
| ); | ||
|
|
||
| for (const [key, value] of Object.entries(exportData.emailTemplate)) { | ||
| const templateName = | ||
| type === "all" && !extract ? fileName : | ||
| getTypedFilename(key, EMAIL_TEMPLATE_FILE_TYPE).split("json")[0]; | ||
|
|
||
| fileData = extractHtmlAndCssToFiles( | ||
| key, | ||
| templateName, | ||
| value, | ||
| fileData, | ||
| extract, | ||
| includeMeta | ||
| ); | ||
| }; | ||
|
|
||
| if (type === "all") { | ||
| saveJsonToFile(fileData, getFilePath(fileName, true)); | ||
| } | ||
|
|
||
| return true; | ||
| } catch (error) { | ||
| stopProgressIndicator(indicatorId, `${error}`); | ||
| printError(error); | ||
| } | ||
| return false; | ||
|
|
@@ -287,22 +378,59 @@ export async function importEmailTemplateFromFile( | |
| 1, | ||
| `Importing ${templateId}` | ||
| ); | ||
|
|
||
| if ( | ||
| (fileData.emailTemplate && fileData.emailTemplate[templateId]) || | ||
| (raw && getTemplateIdFromFileName(file) === templateId) | ||
| ) { | ||
| try { | ||
| const emailTemplateData = raw | ||
| ? s2sConvert(fileData) | ||
| : fileData.emailTemplate[templateId]; | ||
| await updateEmailTemplate(templateId, emailTemplateData); | ||
| updateProgressIndicator(indicatorId, `Importing ${templateId}`); | ||
| stopProgressIndicator(indicatorId, `Imported ${templateId}`); | ||
| return true; | ||
| } catch (error) { | ||
| stopProgressIndicator(indicatorId, `${error}`); | ||
| printError(error); | ||
| const emailTemplate = fileData.emailTemplate[templateId]; | ||
| const isAdvanced = emailTemplate.advancedEditor; | ||
| const removeExtension = file.split("json")[0]; | ||
|
|
||
| // Subject is used to find the locales because this should always exist | ||
| const locales = Object.keys(emailTemplate.subject ?? {}); | ||
|
|
||
| if (raw) { | ||
| fileData.emailTemplate[templateId]._id = `emailTemplate/${templateId}`; | ||
| } | ||
|
|
||
| locales.forEach((locale) => { | ||
| const htmlFilePath = getFilePath(removeExtension + locale + ".html"); | ||
| const htmlData = fs.readFileSync(htmlFilePath, 'utf8'); | ||
| const htmlProperty = isAdvanced ? "message" : "html"; | ||
|
|
||
| if (!emailTemplate[htmlProperty]) { | ||
| emailTemplate[htmlProperty] = {}; | ||
| } | ||
|
|
||
| emailTemplate[htmlProperty][locale] = htmlData; | ||
| }); | ||
|
|
||
| // It's possble that the css file doesn't exist regardless of if the | ||
| // email template is advanced or not | ||
| try { | ||
| const cssFilePath = getFilePath(removeExtension + "css"); | ||
| emailTemplate.styles = fs.readFileSync(cssFilePath, 'utf8'); | ||
| } catch(_) { | ||
| // Do nothing | ||
| } | ||
|
|
||
| fileData.emailTemplate[templateId] = emailTemplate; | ||
|
|
||
| await importEmailTemplates(fileData); | ||
| } catch (_) { | ||
| if (raw) { | ||
| const emailTemplateData = raw | ||
| ? s2sConvert(fileData) | ||
| : fileData.emailTemplate[templateId]; | ||
| await updateEmailTemplate(templateId, emailTemplateData); | ||
| } else { | ||
| await importEmailTemplates(fileData); | ||
| } | ||
| } | ||
|
|
||
| stopProgressIndicator(indicatorId, `Imported ${templateId}`); | ||
| } else { | ||
| stopProgressIndicator( | ||
| indicatorId, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: I would reword this to
Do not extract HTML and CSS to separate files