fix(*): Resolve a variety of small and simple bugs

Summary:
Fix T1822 - saving templates not working, not showing template

Fix T1800 - give composers a minimum size

Fix the bottom bar of the composer so the gray bar goes all the way across in popout mode.

Fix T1825 - switch to a more attractive "June 4, 2015 at 3:10 PM" styling for expanded dates

Remove, rather than hide, react components for text fields in composer. Fixes T1147

Fix specs

Switch to 999+ instead of infinity. Fixes T1768

Fix broken TemplateStore specs

Test Plan: Run tests

Reviewers: evan

Reviewed By: evan

Maniphest Tasks: T1147, T1768, T1822, T1800, T1825

Differential Revision: https://phab.nylas.com/D1601
This commit is contained in:
Ben Gotow 2015-06-05 11:02:44 -07:00
parent 3201a9e82e
commit 503631e685
12 changed files with 123 additions and 71 deletions

View file

@ -159,44 +159,7 @@ class ComposerView extends React.Component
</div>
<ParticipantsTextField
ref="textFieldTo"
field='to'
visible={true}
change={@_onChangeParticipants}
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='102'/>
<ParticipantsTextField
ref="textFieldCc"
field='cc'
visible={@state.showcc}
change={@_onChangeParticipants}
onEmptied={=> @setState showcc: false}
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='103'/>
<ParticipantsTextField
ref="textFieldBcc"
field='bcc'
visible={@state.showbcc}
change={@_onChangeParticipants}
onEmptied={=> @setState showbcc: false}
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='104'/>
<div className="compose-subject-wrap"
style={display: @state.showsubject and 'initial' or 'none'}>
<input type="text"
key="subject"
name="subject"
tabIndex="108"
placeholder="Subject:"
disabled={not @state.showsubject}
className="compose-field compose-subject"
value={@state.subject}
onChange={@_onChangeSubject}/>
</div>
{@_renderFields()}
<div className="compose-body">
<ContenteditableComponent ref="contentBody"
@ -218,6 +181,58 @@ class ComposerView extends React.Component
</div>
</div>
_renderFields: =>
# Note: We need to physically add and remove these elements, not just hide them.
# If they're hidden, shift-tab between fields breaks.
fields = []
fields.push(
<ParticipantsTextField
ref="textFieldTo"
field='to'
change={@_onChangeParticipants}
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='102'/>
)
if @state.showcc
fields.push(
<ParticipantsTextField
ref="textFieldCc"
field='cc'
change={@_onChangeParticipants}
onEmptied={=> @setState showcc: false}
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='103'/>
)
if @state.showbcc
fields.push(
<ParticipantsTextField
ref="textFieldBcc"
field='bcc'
change={@_onChangeParticipants}
onEmptied={=> @setState showbcc: false}
participants={to: @state['to'], cc: @state['cc'], bcc: @state['bcc']}
tabIndex='104'/>
)
if @state.showsubject
fields.push(
<div className="compose-subject-wrap">
<input type="text"
key="subject"
name="subject"
tabIndex="108"
placeholder="Subject:"
disabled={not @state.showsubject}
className="compose-field compose-subject"
value={@state.subject}
onChange={@_onChangeSubject}/>
</div>
)
fields
_renderFooterRegions: =>
return <div></div> unless @props.localId

View file

@ -43,12 +43,13 @@ class ContenteditableComponent extends React.Component
range = @_getRangeInScope()
for extension in DraftStore.extensions()
extension.onFocusNext(editableNode, range, event) if extension.onFocusNext
'core:focus-previous': (event) =>
editableNode = @_editableNode()
range = @_getRangeInScope()
for extension in DraftStore.extensions()
extension.onFocusPrevious(editableNode, range, event) if extension.onFocusPrevious
}
}
@_cleanHTML()

View file

@ -55,6 +55,7 @@ module.exports =
ComponentRegistry.register ComposeButton,
location: WorkspaceStore.Location.RootSidebar.Toolbar
else
atom.getCurrentWindow().setMinimumSize(600, 400)
WorkspaceStore.defineSheet 'Main', {root: true},
list: ['Center']
ComponentRegistry.register ComposerWithWindowProps,

View file

@ -17,9 +17,6 @@ class ParticipantsTextField extends React.Component
# to modify the `participants` provided.
field: React.PropTypes.string,
# Whether or not the field should be visible. Defaults to true.
visible: React.PropTypes.bool
# An object containing arrays of participants. Typically, this is
# {to: [], cc: [], bcc: []}. Each ParticipantsTextField needs all of
# the values, because adding an element to one field may remove it
@ -36,7 +33,7 @@ class ParticipantsTextField extends React.Component
render: =>
classSet = {}
classSet[@props.field] = true
<div className="participants-text-field" style={zIndex: 1000-@props.tabIndex, display: @props.visible and 'inline' or 'none'}>
<div className="participants-text-field" style={zIndex: 1000-@props.tabIndex, display: 'inline'}>
<TokenizingTextField
ref="textField"
tokens={@props.participants[@props.field]}

View file

@ -182,7 +182,7 @@ describe "populated composer", ->
describe "when focus() is called", ->
describe "if a field name is provided", ->
it "should focus that field", ->
useDraft.call(@)
useDraft.call(@, cc: [u2])
makeComposer.call(@)
spyOn(@composer.refs['textFieldCc'], 'focus')
@composer.focus('textFieldCc')

View file

@ -28,15 +28,15 @@
width: 100%;
background: transparent;
border-bottom: 0;
max-width: @compose-width;
margin: 0 auto;
.composer-action-bar-content {
display:flex;
margin: 0 auto;
flex-direction:row;
margin-left: -@spacing-standard / 2;
margin-right: -@spacing-standard / 2;
padding: @spacing-standard @spacing-double;
max-width: @compose-width;
padding-left: @spacing-standard + @spacing-standard / 2;
padding-right: @spacing-standard + @spacing-standard / 2;
padding-top: @spacing-standard;
padding-bottom: @spacing-standard*1.1;
> * {
@ -109,7 +109,7 @@
margin: 0 @spacing-standard;
border-bottom: 1px solid @border-color-divider;
flex-shrink:0;
.subject-label {
color: @text-color-very-subtle;
float: left;

View file

@ -23,7 +23,7 @@ class MessageTimestamp extends React.Component
_timeFormat: =>
if @props.isDetailed
return "DD / MM / YYYY h:mm a z"
return "MMMM D, YYYY [at] h:mm A"
else
today = moment(@_today())
dayOfEra = today.dayOfYear() + today.year() * 365

View file

@ -38,4 +38,4 @@ describe "MessageTimestamp", ->
<MessageTimestamp date={testDate()} isDetailed={true} />
)
spyOn(itemDetailed, "_today").andCallFake -> testDate()
expect(itemDetailed._timeFormat()).toBe "DD / MM / YYYY h:mm a z"
expect(itemDetailed._timeFormat()).toBe "MMMM D, YYYY [at] h:mm A"

View file

@ -37,7 +37,7 @@ class TemplatePicker extends React.Component
]
footerComponents = [
<div className="item" key="new" onMouseDown={@_onNewTemplate}>Save as Template...</div>
<div className="item" key="new" onMouseDown={@_onNewTemplate}>Save Draft as Template...</div>
<div className="item" key="manage" onMouseDown={@_onManageTemplates}>Open Templates Folder...</div>
]

View file

@ -1,6 +1,7 @@
Reflux = require 'reflux'
_ = require 'underscore'
{DatabaseStore, DraftStore, Actions, Message} = require 'nylas-exports'
shell = require 'shell'
path = require 'path'
fs = require 'fs-plus'
@ -59,21 +60,32 @@ TemplateStore = Reflux.createStore
draft = session.draft()
name ?= draft.subject
contents ?= draft.body
if not name or name.length is 0
return @_displayError("Give your draft a subject to name your template.")
if not contents or contents.length is 0
return @_displayError("To create a template you need to fill the body of the current draft.")
@_writeTemplate(name, contents)
else
if not name or name.length is 0
return @_displayError("You must provide a name for your template.")
if not contents or contents.length is 0
return @_displayError("You must provide contents for your template.")
@_writeTemplate(name, contents)
_onShowTemplates: ->
# show in finder how?
shell = require 'shell'
shell.showItemInFolder(@_items[0]?.path || @_templatesDir)
_displayError: (message) ->
dialog = require('remote').require('dialog')
dialog.showErrorBox('Template Creation Error', message)
_writeTemplate: (name, contents) ->
throw new Error("You must provide a template name") unless name
throw new Error("You must provide template contents") unless contents
filename = "#{name}.html"
templatePath = path.join(@_templatesDir, filename)
fs.writeFile templatePath, contents, (err) =>
@_displayError(err) if err
shell.showItemInFolder(templatePath)
@_items.push
id: filename,
name: name,

View file

@ -18,6 +18,9 @@ stubTemplates = [
describe "TemplateStore", ->
beforeEach ->
spyOn(fs, 'mkdir')
spyOn(shell, 'showItemInFolder').andCallFake ->
spyOn(fs, 'writeFile').andCallFake (path, contents, callback) ->
callback(null)
spyOn(fs, 'readFile').andCallFake (path, callback) ->
filename = path.split('/').pop()
callback(null, stubTemplateFiles[filename])
@ -71,9 +74,15 @@ describe "TemplateStore", ->
describe "onCreateTemplate", ->
beforeEach ->
spyOn(fs, 'readdir').andCallFake (path, callback) -> callback(null, [])
spyOn(fs, 'writeFile').andCallFake (path, contents, callback) -> callback(null)
TemplateStore.init()
spyOn(DraftStore, 'sessionForLocalId').andCallFake (draftLocalId) ->
if draftLocalId is 'localid-nosubject'
d = new Message(subject: '', body: '<p>Body</p>')
else
d = new Message(subject: 'Subject', body: '<p>Body</p>')
session =
draft: -> d
Promise.resolve(session)
it "should create a template with the given name and contents", ->
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
@ -82,11 +91,15 @@ describe "TemplateStore", ->
expect(item.name).toBe "123"
expect(item.path.split("/").pop()).toBe "123.html"
it "should throw an exception if no name is provided", ->
expect( -> TemplateStore._onCreateTemplate({contents: 'bla'})).toThrow()
it "should display an error if no name is provided", ->
spyOn(TemplateStore, '_displayError')
TemplateStore._onCreateTemplate({contents: 'bla'})
expect(TemplateStore._displayError).toHaveBeenCalled()
it "should throw an exception if no content is provided", ->
expect( -> TemplateStore._onCreateTemplate({name: 'bla'})).toThrow()
it "should display an error if no content is provided", ->
spyOn(TemplateStore, '_displayError')
TemplateStore._onCreateTemplate({name: 'bla'})
expect(TemplateStore._displayError).toHaveBeenCalled()
it "should save the template file to the templates folder", ->
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
@ -95,17 +108,30 @@ describe "TemplateStore", ->
expect(fs.writeFile.mostRecentCall.args[0]).toEqual(path)
expect(fs.writeFile.mostRecentCall.args[1]).toEqual('bla')
it "should open the template so you can see it", ->
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
path = "#{stubTemplatesDir}/123.html"
expect(shell.showItemInFolder).toHaveBeenCalled()
describe "when given a draft id", ->
it "should create a template from the name and contents of the given draft", ->
draft = new Message
subject: 'Subject'
body: '<p>Body</p>'
spyOn(DatabaseStore, 'findByLocalId').andReturn(Promise.resolve(draft))
TemplateStore._onCreateTemplate({draftLocalId: 'localid-b'})
expect(TemplateStore.items()).toEqual([])
runs ->
TemplateStore._onCreateTemplate({draftLocalId: 'localid-b'})
waitsFor ->
fs.writeFile.callCount > 0
runs ->
expect(TemplateStore.items().length).toEqual(1)
it "should display an error if the draft has no subject", ->
spyOn(TemplateStore, '_displayError')
runs ->
TemplateStore._onCreateTemplate({draftLocalId: 'localid-nosubject'})
waitsFor ->
TemplateStore._displayError.callCount > 0
runs ->
expect(TemplateStore._displayError).toHaveBeenCalled()
describe "onShowTemplates", ->
it "should open the templates folder in the Finder", ->
spyOn(shell, 'showItemInFolder')
TemplateStore._onShowTemplates()
expect(shell.showItemInFolder).toHaveBeenCalled()

View file

@ -36,7 +36,7 @@ AppUnreadBadgeStore = Reflux.createStore
AppUnreadCount = count
if count > 999
app.dock?.setBadge?("\u221E")
app.dock?.setBadge?("999+")
else if count > 0
app.dock?.setBadge?("#{count}")
else