fix(templates): fix several bugs in templates plugin

Fixes behavior when there are no template files, prevents renaming/creating
with an empty name, fixes yet another way to accidentally make yellow text,
misc small style fixes
This commit is contained in:
Drew Regitsky 2016-01-12 12:44:22 -08:00
parent 57aee26256
commit c91a0c6062
6 changed files with 58 additions and 32 deletions

View file

@ -13,7 +13,7 @@ class PreferencesTemplates extends React.Component
{templates, selectedTemplate, selectedTemplateName} = @_getStateFromStores()
@state =
editAsHTML: false
editState: null
editState: if templates.length==0 then "new" else null
templates: templates
selectedTemplate: selectedTemplate
selectedTemplateName: selectedTemplateName
@ -108,14 +108,14 @@ class PreferencesTemplates extends React.Component
_renderEditableTemplate: ->
<Contenteditable
ref="templateInput"
value={@state.contents}
value={@state.contents || ""}
onChange={@_onEditTemplate}
extensions={[TemplateEditor]}
spellcheck={false} />
_renderHTMLTemplate: ->
<textarea ref="templateHTMLInput"
value={@state.contents}
value={@state.contents || ""}
onChange={@_onEditTemplate}/>
_renderModeToggle: ->
@ -164,8 +164,15 @@ class PreferencesTemplates extends React.Component
# DELETE AND NEW
_deleteTemplate: =>
numTemplates = @state.templates.length
if @state.selectedTemplate?
TemplateStore.deleteTemplate(@state.selectedTemplate.name)
if numTemplates==1
@setState
editState: "new"
selectedTemplate: null
selectedTemplateName: ""
contents: ""
_startNewTemplate: =>
@setState
@ -175,7 +182,7 @@ class PreferencesTemplates extends React.Component
contents: ""
_saveNewTemplate: =>
TemplateStore.saveNewTemplate(@state.selectedTemplateName, @state.contents, (template) =>
TemplateStore.saveNewTemplate(@state.selectedTemplateName, @state.contents || "", (template) =>
@setState
selectedTemplate: template
editState: null
@ -190,10 +197,11 @@ class PreferencesTemplates extends React.Component
@_loadTemplateContents(template)
_renderCreateNew: ->
cancel = <button className="btn template-name-btn" onClick={@_cancelNewTemplate}>Cancel</button>
<div className="section-title">
Template Name: <input type="text" className="template-name-input" value={@state.selectedTemplateName} onChange={@_onEditName}/>
<button className="btn btn-emphasis template-name-btn" onClick={@_saveNewTemplate}>Save</button>
<button className="btn template-name-btn" onClick={@_cancelNewTemplate}>Cancel</button>
{if @state.templates.length then cancel}
</div>
@ -218,6 +226,11 @@ class PreferencesTemplates extends React.Component
</div>
</div>
noTemplatesMessage =
<div className="template-status-bar no-templates-message">
You don't have any templates! Enter a template name and press Save to create one.
</div>
<div>
<section className="container-templates" style={if @state.editState is "new" then {marginBottom:50}}>
<h2>Quick Replies</h2>
@ -228,22 +241,23 @@ class PreferencesTemplates extends React.Component
else @_renderName()
}
{if @state.editState isnt "new" then editor}
{if @state.editState is "new" then noTemplatesMessage}
</section>
<section className="templates-instructions">
<p>
The Quick Replies plugin allows you to create templated email replies. Replies can contain variables, which
you can quickly jump between and fill out when using the template. To create a variable, type a set of double curly
The Quick Replies plugin allows you to create templated email replies, with variables that
you can quickly fill out inside your email message. To create a variable, type a set of double curly
brackets wrapping the variable's name, like this: <strong>{"{{"}variable_name{"}}"}</strong>
</p>
<p>
Reply templates are saved in the <strong>~/.nylas/templates</strong> directory on your computer. Each template
is an HTML file - the name of the file is the name of the template, and its contents are the default message body.
</p>
<p>
In raw HTML, variables are defined as HTML &lt;code&gt; tags with class "var empty". Typing curly brackets creates a tag
automatically. The code tags are colored yellow to show the variable regions, but will be stripped out before the message is sent.
</p>
<p>
Reply templates live in the <strong>~/.nylas/templates</strong> directory on your computer. Each template
is an HTML file - the name of the file is the name of the template, and its contents are the default message body.
</p>
</section>
</div>

View file

@ -18,11 +18,21 @@ class TemplateEditor extends ContenteditableExtension
editor.whilePreservingSelection ->
DOMUtils.unwrapNode(codeNode)
# Attempt to sanitize spans that are needlessly created by contenteditable
for span in editor.rootNode.querySelectorAll("span")
if not span.className
# Attempt to sanitize extra nodes that may have been created by contenteditable on certain text editing
# operations (insertion/deletion of line breaks, etc.). These are generally <span>, but can also be
# <font>, <b>, and possibly others. The extra nodes often grab CSS styles from neighboring elements
# as inline style, including the yellow text from <code> nodes that we insert. This is contenteditable
# trying to be "smart" and preserve styles, which is very undesirable for the <code> node styles. The
# below code is a hack to prevent yellow text from appearing.
for node in editor.rootNode.querySelectorAll("*")
if not node.className and node.style.color == "#c79b11"
editor.whilePreservingSelection ->
DOMUtils.unwrapNode(span)
DOMUtils.unwrapNode(node)
for node in editor.rootNode.querySelectorAll("font")
if node.color == "#c79b11"
editor.whilePreservingSelection ->
DOMUtils.unwrapNode(node)
# Find all {{}} and wrap them in code nodes if they aren't already
# Regex finds any {{ <contents> }} that doesn't contain {, }, or \n
@ -39,16 +49,4 @@ class TemplateEditor extends ContenteditableExtension
editor.restoreSelectionByTextIndex(codeNode, selIndex.startIndex, selIndex.endIndex)
@onKeyDown: ({editor}) ->
# Look for all existing code tags that we may have added before,
# and remove any that now have invalid content (don't start with {{ and
# end with }} as well as any that wrap the current selection
codeNodes = editor.rootNode.querySelectorAll("code.var.empty")
for codeNode in codeNodes
text = codeNode.textContent
if not text.startsWith("{{") or not text.endsWith("}}") or DOMUtils.selectionStartsOrEndsIn(codeNode)
editor.whilePreservingSelection ->
DOMUtils.unwrapNode(codeNode)
module.exports = TemplateEditor

View file

@ -32,7 +32,7 @@ class TemplateStatusBar extends React.Component {
static containerStyles = {
textAlign: 'center',
width: 530,
width: 580,
margin: 'auto',
}

View file

@ -147,6 +147,11 @@ class TemplateStore extends NylasStore {
}
saveNewTemplate(name, contents, callback) {
if(!name || name.length===0){
this._displayError('You must provide a template name.');
return;
}
if (name.match(TemplateStore.INVALID_TEMPLATE_NAME_REGEX)) {
this._displayError('Invalid template name! Names can only contain letters, numbers, spaces, dashes, and underscores.');
return;
@ -219,8 +224,12 @@ class TemplateStore extends NylasStore {
this._displayError('Invalid template name! Names can only contain letters, numbers, spaces, dashes, and underscores.');
return;
}
if(newName.length===0){
this._displayError('You must provide a template name.');
return;
}
const newFilename = `${newName}.html`;
const newFilename = `${newName}.html`;
const oldPath = path.join(this._templatesDir, `${oldName}.html`);
const newPath = path.join(this._templatesDir, newFilename);
fs.rename(oldPath, newPath, () => {

View file

@ -43,6 +43,11 @@
max-width: 640px;
.no-templates-message {
text-align: center;
margin-top: 50px;
}
.template-wrap {
position: relative;
border: 1px solid @input-border-color;
@ -112,7 +117,7 @@
.template-name-btn {
float: right;
margin: 6px;
margin: 0 6px;
}
.template-name-input {
display: inline-block;

View file

@ -538,7 +538,7 @@ DOMUtils =
# current selection.
selectionStartsOrEndsIn: (rangeOrNode) ->
selection = document.getSelection()
return false unless selection
return false unless (selection and selection.rangeCount>0)
if rangeOrNode instanceof Range
return @rangeStartsOrEndsInRange(selection.getRangeAt(0), rangeOrNode)
else if rangeOrNode instanceof Node
@ -552,7 +552,7 @@ DOMUtils =
# contained within it.
selectionIsWithin: (rangeOrNode) ->
selection = document.getSelection()
return false unless selection
return false unless (selection and selection.rangeCount>0)
if rangeOrNode instanceof Range
return @rangeInRange(selection.getRangeAt(0), rangeOrNode)
else if rangeOrNode instanceof Node