Skip to content

Commit

Permalink
Merge pull request #342 from mjmlio/templating
Browse files Browse the repository at this point in the history
Templating
  • Loading branch information
kmcb777 authored Feb 18, 2022
2 parents e3b7051 + 333764e commit 4a5d283
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 27 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,15 @@
"codemirror": "^5.49.2",
"electron-debug": "^3.0.1",
"electron-json-storage": "^4.1.8",
"erb": "^1.3.0-hf.1",
"electron-updater": "4.3.9",
"es6-promisify": "^6.0.2",
"fix-path": "^2.1.0",
"fuse.js": "^3.4.5",
"handlebars": "4.7.7",
"immutable": "^4.0.0-rc.12",
"js-beautify": "^1.10.2",
"js-yaml": "^3.14.0",
"mjml": "^4.12.0",
"mjml-migrate": "^4.12.0",
"ncp": "^2.0.0",
Expand Down
1 change: 1 addition & 0 deletions src/actions/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function loadSettings() {
desktop: 650,
},
snippets: [],
templating: [],
})

// clean old format for TargetEmails
Expand Down
44 changes: 43 additions & 1 deletion src/components/FilesList/FilePreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,66 @@ import React, { Component } from 'react'
import cx from 'classnames'
import { Motion, spring } from 'react-motion'
import { connect } from 'react-redux'
import isEqual from 'lodash/isEqual'
import find from 'lodash/find'

import Button from 'components/Button'
import Iframe from 'components/Iframe'

import { updateSettings } from 'actions/settings'
import { addAlert } from 'reducers/alerts'
import { compile } from 'helpers/preview-content'

export default connect(
state => ({
preview: state.preview,
previewSize: state.settings.get('previewSize'),
templating: state.settings.get('templating'),
}),
{
updateSettings,
addAlert,
},
)(
class FilePreview extends Component {
state = {
content: '',
}

componentDidUpdate(prevProps) {
const prev = {
engine: this.getProjectVariables(prevProps).engine,
variables: this.getProjectVariables(prevProps).variables,
raw: prevProps.preview ? prevProps.preview.content : '',
}

const current = {
engine: this.getProjectVariables(this.props).engine,
variables: this.getProjectVariables(this.props).variables,
raw: this.props.preview ? this.props.preview.content : '',
}

!isEqual(prev, current) && this.updateContent(current)
}

getProjectVariables = props => {
const { templating, iframeBase } = props
return find(templating, { projectPath: iframeBase }) || {}
}

updateContent = async params => {
try {
const content = await compile(params)
this.setState({ content })
} catch (err) {
this.props.addAlert(`[Template Compiler Error] ${err.message}`, 'error')
throw new Error(err)
}
}

render() {
const { preview, disablePointer, previewSize, onSetSize, iframeBase } = this.props
const { content } = this.state

return (
<div className="FilesList--preview">
Expand Down Expand Up @@ -55,7 +97,7 @@ export default connect(
</div>
{preview ? (
preview.type === 'html' ? (
<Iframe base={iframeBase} value={preview.content} openLinks />
<Iframe base={iframeBase} value={content} openLinks />
) : preview.type === 'image' ? (
<img className="FileList--preview-image" src={`file://${preview.content}`} />
) : null
Expand Down
19 changes: 19 additions & 0 deletions src/components/FilesList/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,22 @@
color: $yellow;
padding: 20px;
}

.FilePreview--settings {
&-button {
position: absolute;
top: 0;
right: 10px;
pointer-events: auto;
}

&-modal {
.bordered {
border: 1px solid $evenLighterGrey;
}

.yaml-invalid {
color: $red;
}
}
}
20 changes: 20 additions & 0 deletions src/helpers/preview-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import erb from 'erb'
import Handlebars from 'handlebars'

export const compile = async ({ raw, engine, variables = {} }) => {
if (engine === 'erb') {
const res = await erb({
timeout: 5000,
data: { values: variables },
template: raw,
})

return res
}
if (engine === 'handlebars') {
const res = Handlebars.compile(raw)(variables)
return res
}

return raw
}
26 changes: 13 additions & 13 deletions src/helpers/takeScreenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ export function takeScreenshot(html, deviceWidth, workingDirectory) {

win.webContents.on('did-finish-load', () => {
// Window is not fully loaded after this event, hence setTimeout()...
win.webContents.executeJavaScript(
"document.querySelector('body').getBoundingClientRect().height"
).then(height => {
win.setSize(deviceWidth, height + 50)
const takeShot = () => {
win.webContents.capturePage().then(img => {
// eslint-disable-line
win.close()
resolve(img.toPNG())
})
}
setTimeout(takeShot, 500)
})
win.webContents
.executeJavaScript("document.querySelector('body').getBoundingClientRect().height")
.then(height => {
win.setSize(deviceWidth, height + 50)
const takeShot = () => {
win.webContents.capturePage().then(img => {
// eslint-disable-line
win.close()
resolve(img.toPNG())
})
}
setTimeout(takeShot, 500)
})
})
})
}
Expand Down
205 changes: 205 additions & 0 deletions src/pages/Project/PreviewSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import debounce from 'lodash/debounce'
import find from 'lodash/find'
import yaml from 'js-yaml'

import CodeMirror from 'codemirror/lib/codemirror'
import 'codemirror/mode/yaml/yaml'

import Modal from 'components/Modal'

import { updateSettings } from 'actions/settings'
import { addAlert } from 'reducers/alerts'

const defaultTemplatingSettings = projectPath => ({
variables: {},
engine: 'html',
editorMode: 'json',
projectPath,
})

export default connect(
state => ({
templating: state.settings.get('templating'),
lightTheme: state.settings.getIn(['editor', 'lightTheme'], false),
}),
{
addAlert,
updateSettings,
},
)(
class PreviewSettings extends Component {
state = {
variables: defaultTemplatingSettings().variables,
engine: defaultTemplatingSettings().engine,
editorMode: defaultTemplatingSettings().editorMode,
valid: true,
}

componentDidUpdate(prevProps) {
if (!prevProps.isOpened && this.props.isOpened) {
this.initEditor()
}

if (
prevProps.templating !== this.props.templating ||
(!prevProps.isOpened && this.props.isOpened)
) {
this.setState({
...defaultTemplatingSettings(this.props.currentProjectPath),
...this.currentProjectTemplating(),
})
}
}

currentProjectTemplating() {
const { currentProjectPath, templating } = this.props
const projectTemplating = find(templating, { projectPath: currentProjectPath })

return projectTemplating || defaultTemplatingSettings(currentProjectPath)
}

handleChangeEngine = event => {
this.setState({
engine: event.target.value,
})
}

handleEditorMode = event => {
const { value } = event.target
this.setState({
editorMode: value,
})
this._codeMirror.setOption('mode', value)
this.handleChangeVars()
}

handleChangeVars = debounce(() => {
const raw = this._codeMirror.getValue()
try {
switch (this.state.editorMode) {
case 'yaml':
this.setState({ variables: yaml.safeLoad(raw) || {}, valid: true })
break
case 'json':
this.setState({ variables: JSON.parse(raw) || {}, valid: true })
break
default:
this.setState({ variables: raw || {}, valid: true })
break
}
} catch (err) {
this.setState({ valid: false })
}
}, 200)

saveVars = () => {
if (!this.state.valid) {
this.props.addAlert('Invalid variables syntax, couldn’t be saved', 'error')
return
}

const { templating, currentProjectPath } = this.props
const otherProjectVariables = templating.filter(v => v.projectPath !== currentProjectPath)
const updatedVariables = [
...otherProjectVariables,
{
projectPath: currentProjectPath,
variables: this.state.variables,
engine: this.state.engine,
editorMode: this.state.editorMode,
},
]

this.props.updateSettings(settings => {
return settings.set('templating', updatedVariables)
})
}

initEditor() {
if (!this._textarea) return

const { lightTheme } = this.props
const { variables, editorMode } = this.currentProjectTemplating()

if (this._codeMirror) {
this._codeMirror.toTextArea()
this._codeMirror = null
}

let content = ''

try {
if (Object.keys(variables).length > 0) {
if (editorMode === 'yaml') content = yaml.safeDump(variables)
if (editorMode === 'json') content = JSON.stringify(variables, null, 2)
}

this.setState({ variables, valid: true })
} catch (err) {
this.props.addAlert('Initial variables cannot be serialized', 'error')
}

this._codeMirror = CodeMirror.fromTextArea(this._textarea, {
tabSize: 2,
dragDrop: false,
mode: editorMode,
lineNumbers: false,
theme: lightTheme ? 'neo' : 'one-dark',
})

this._codeMirror.setValue(content)

this._codeMirror.on('change', this.handleChangeVars)
}

handleClose() {
this.saveVars()
this.props.onClose()
}

render() {
const { isOpened, onClose } = this.props
const { valid, engine, editorMode } = this.state

return (
<Modal
isOpened={isOpened}
onClose={() => this.handleClose()}
className="FilePreview--settings-modal p-10 d-f fd-c"
>
<div className="Modal--label">{'Preview settings'}</div>

<div className="mb-10">Define templating variables for this project :</div>
<div className="mb-10">
{'Treat MJML output as '}

<select value={engine} className="bordered" onChange={this.handleChangeEngine}>
<option value="html">HTML</option>
<option value="erb">ERB template</option>
<option value="handlebars">Handlebars template</option>
</select>
</div>

<div className="">
{'Define variables using '}

<select value={editorMode} className="bordered" onChange={this.handleEditorMode}>
<option value="json">JSON</option>
<option value="yaml">YAML</option>
</select>
</div>

<div className="mt-10 d-f jc-sb">
<div>{`Template variables (${editorMode}):`}</div>
<div className="yaml-invalid">{!valid && `× Invalid ${editorMode}`}</div>
</div>
<div className="bordered mt-5">
<textarea ref={r => (this._textarea = r)} />
</div>
</Modal>
)
}
},
)
Loading

0 comments on commit 4a5d283

Please sign in to comment.