fix(composer): Refactor header actions, clean up layout

Summary:
WIP

Remove the mode prop from everywhere, use NylasEnv.isComposerWindow() instead

Test Plan: Run updated tests

Reviewers: drew, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2766
This commit is contained in:
Ben Gotow 2016-03-21 11:32:48 -07:00
parent 400ccb1cdb
commit b5fe01e5d0
9 changed files with 219 additions and 157 deletions

View file

@ -0,0 +1,55 @@
React = require 'react'
Fields = require './fields'
{Actions} = require 'nylas-exports'
{RetinaImg} = require 'nylas-component-kit'
module.exports =
class ComposerHeaderActions extends React.Component
@displayName: 'ComposerHeaderActions'
@propTypes:
draftClientId: React.PropTypes.string.isRequired
focusedField: React.PropTypes.string
enabledFields: React.PropTypes.array.isRequired
onAdjustEnabledFields: React.PropTypes.func.isRequired
render: =>
items = []
if @props.focusedField in Fields.ParticipantFields
if Fields.Cc not in @props.enabledFields
items.push(
<span className="action show-cc" key="cc"
onClick={ => @props.onAdjustEnabledFields(show: [Fields.Cc]) }>Cc</span>
)
if Fields.Bcc not in @props.enabledFields
items.push(
<span className="action show-bcc" key="bcc"
onClick={ => @props.onAdjustEnabledFields(show: [Fields.Bcc]) }>Bcc</span>
)
if Fields.Subject not in @props.enabledFields
items.push(
<span className="action show-subject" key="subject"
onClick={ => @props.onAdjustEnabledFields(show: [Fields.Subject]) }>Subject</span>
)
unless NylasEnv.isComposerWindow()
items.push(
<span className="action show-popout" key="popout"
title="Popout composer…"
style={paddingLeft: "1.5em"}
onClick={@_onPopoutComposer}>
<RetinaImg name="composer-popout.png"
mode={RetinaImg.Mode.ContentIsMask}
style={{position: "relative", top: "-2px"}}/>
</span>
)
<div className="composer-header-actions">
{items}
</div>
_onPopoutComposer: =>
Actions.composePopoutDraft(@props.draftClientId)

View file

@ -26,6 +26,7 @@ FileUpload = require './file-upload'
ImageFileUpload = require './image-file-upload' ImageFileUpload = require './image-file-upload'
ComposerEditor = require './composer-editor' ComposerEditor = require './composer-editor'
ComposerHeaderActions = require './composer-header-actions'
SendActionButton = require './send-action-button' SendActionButton = require './send-action-button'
ExpandedParticipants = require './expanded-participants' ExpandedParticipants = require './expanded-participants'
CollapsedParticipants = require './collapsed-participants' CollapsedParticipants = require './collapsed-participants'
@ -43,9 +44,6 @@ class ComposerView extends React.Component
@propTypes: @propTypes:
draftClientId: React.PropTypes.string draftClientId: React.PropTypes.string
# Either "inline" or "fullwindow"
mode: React.PropTypes.string
# If this composer is part of an existing thread (like inline # If this composer is part of an existing thread (like inline
# composers) the threadId will be handed down # composers) the threadId will be handed down
threadId: React.PropTypes.string threadId: React.PropTypes.string
@ -207,37 +205,46 @@ class ComposerView extends React.Component
</DropZone> </DropZone>
_renderScrollRegion: -> _renderScrollRegion: ->
if @props.mode is "inline" if NylasEnv.isComposerWindow()
@_renderContent()
else
<ScrollRegion className="compose-body-scroll" ref="scrollregion"> <ScrollRegion className="compose-body-scroll" ref="scrollregion">
{@_renderContent()} {@_renderContent()}
</ScrollRegion> </ScrollRegion>
else
@_renderContent()
_renderContent: => _renderContent: =>
<div className="composer-centered"> <div className="composer-centered">
{if @state.focusedField in Fields.ParticipantFields <div className="composer-header">
<ExpandedParticipants {if @state.draftReady
to={@state.to} cc={@state.cc} bcc={@state.bcc} <ComposerHeaderActions
from={@state.from} draftClientId={@props.draftClientId}
ref="expandedParticipants" focusedField={@state.focusedField}
mode={@props.mode} enabledFields={@state.enabledFields}
accounts={@state.accounts} onAdjustEnabledFields={@_onAdjustEnabledFields}
draftReady={@state.draftReady} />
focusedField={@state.focusedField} }
enabledFields={@state.enabledFields} {if @state.focusedField in Fields.ParticipantFields
onPopoutComposer={@_onPopoutComposer} <ExpandedParticipants
onChangeParticipants={@_onChangeParticipants} to={@state.to} cc={@state.cc} bcc={@state.bcc}
onChangeFocusedField={@_onChangeFocusedField} from={@state.from}
onAdjustEnabledFields={@_onAdjustEnabledFields} /> ref="expandedParticipants"
else accounts={@state.accounts}
<CollapsedParticipants draftReady={@state.draftReady}
to={@state.to} cc={@state.cc} bcc={@state.bcc} focusedField={@state.focusedField}
onClick={@_onExpandParticipantFields} /> enabledFields={@state.enabledFields}
} onPopoutComposer={@_onPopoutComposer}
onChangeParticipants={@_onChangeParticipants}
{@_renderSubject()} onChangeFocusedField={@_onChangeFocusedField}
onAdjustEnabledFields={@_onAdjustEnabledFields} />
else
<CollapsedParticipants
to={@state.to} cc={@state.cc} bcc={@state.bcc}
onPopoutComposer={@_onPopoutComposer}
onClick={@_onExpandParticipantFields} />
}
{@_renderSubject()}
</div>
<div className="compose-body" <div className="compose-body"
ref="composeBody" ref="composeBody"
onMouseUp={@_onMouseUpComposerBody} onMouseUp={@_onMouseUpComposerBody}
@ -247,10 +254,6 @@ class ComposerView extends React.Component
</div> </div>
</div> </div>
_onPopoutComposer: =>
return unless @state.draftReady
Actions.composePopoutDraft @props.draftClientId
_onKeyDown: (event) => _onKeyDown: (event) =>
if event.key is "Tab" if event.key is "Tab"
@_onTabDown(event) @_onTabDown(event)
@ -430,17 +433,22 @@ class ComposerView extends React.Component
_renderActionsRegion: => _renderActionsRegion: =>
return <div></div> unless @props.draftClientId return <div></div> unless @props.draftClientId
<div className="composer-action-bar-content"> <div className="composer-action-bar-content">
<InjectedComponentSet className="composer-action-bar-plugins" <InjectedComponentSet
matching={role: "Composer:ActionButton"} className="composer-action-bar-plugins"
exposedProps={draftClientId:@props.draftClientId, threadId: @props.threadId}></InjectedComponentSet> matching={role: "Composer:ActionButton"}
exposedProps={draftClientId: @props.draftClientId, threadId: @props.threadId} />
<button className="btn btn-toolbar btn-trash" style={order: 100} <button className="btn btn-toolbar btn-trash" style={order: 100}
title="Delete draft" title="Delete draft"
onClick={@_destroyDraft}><RetinaImg name="icon-composer-trash.png" mode={RetinaImg.Mode.ContentIsMask} /></button> onClick={@_destroyDraft}>
<RetinaImg name="icon-composer-trash.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
<button className="btn btn-toolbar btn-attach" style={order: 50} <button className="btn btn-toolbar btn-attach" style={order: 50}
title="Attach file" title="Attach file"
onClick={@_selectAttachment}><RetinaImg name="icon-composer-attachment.png" mode={RetinaImg.Mode.ContentIsMask} /></button> onClick={@_selectAttachment}>
<RetinaImg name="icon-composer-attachment.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
<div style={order: 0, flex: 1} /> <div style={order: 0, flex: 1} />
@ -522,7 +530,7 @@ class ComposerView extends React.Component
return Fields.To if draft.to.length is 0 return Fields.To if draft.to.length is 0
return Fields.Subject if (draft.subject ? "").trim().length is 0 return Fields.Subject if (draft.subject ? "").trim().length is 0
shouldFocusBody = @props.mode isnt 'inline' or draft.pristine or shouldFocusBody = NylasEnv.isComposerWindow() or draft.pristine or
(FocusedContentStore.didFocusUsingClick('thread') is true) (FocusedContentStore.didFocusUsingClick('thread') is true)
return Fields.Body if shouldFocusBody return Fields.Body if shouldFocusBody
return null return null

View file

@ -35,9 +35,6 @@ class ExpandedParticipants extends React.Component
# The account to which the current draft belongs # The account to which the current draft belongs
accounts: React.PropTypes.array accounts: React.PropTypes.array
# Either "fullwindow" or "inline"
mode: React.PropTypes.string
# The field that should be focused # The field that should be focused
focusedField: React.PropTypes.string focusedField: React.PropTypes.string
@ -74,8 +71,7 @@ class ExpandedParticipants extends React.Component
@_applyFocusedField() @_applyFocusedField()
render: -> render: ->
<div className="expanded-participants" <div className="expanded-participants" ref="participantWrap">
ref="participantWrap">
{@_renderFields()} {@_renderFields()}
</div> </div>
@ -92,43 +88,15 @@ class ExpandedParticipants extends React.Component
# If they're hidden, shift-tab between fields breaks. # If they're hidden, shift-tab between fields breaks.
fields = [] fields = []
fields.push( fields.push(
<div key="to"> <ParticipantsTextField
<div className="composer-participant-actions"> ref={Fields.To}
{if Fields.Cc not in @props.enabledFields key="to"
<span className="header-action show-cc" field='to'
onClick={ => @props.onAdjustEnabledFields(show: [Fields.Cc]) }>Cc</span> change={@props.onChangeParticipants}
} className="composer-participant-field to-field"
draftReady={@props.draftReady}
{ if Fields.Bcc not in @props.enabledFields onFocus={ => @props.onChangeFocusedField(Fields.To) }
<span className="header-action show-bcc" participants={to: @props['to'], cc: @props['cc'], bcc: @props['bcc']} />
onClick={ => @props.onAdjustEnabledFields(show: [Fields.Bcc]) }>Bcc</span>
}
{ if Fields.Subject not in @props.enabledFields
<span className="header-action show-subject"
onClick={ => @props.onAdjustEnabledFields(show: [Fields.Subject]) }>Subject</span>
}
{ if @props.mode is "inline"
<span className="header-action show-popout"
title="Popout composer…"
style={paddingLeft: "1.5em"}
onClick={@props.onPopoutComposer}>
<RetinaImg name="composer-popout.png"
mode={RetinaImg.Mode.ContentIsMask}
style={{position: "relative", top: "-2px"}}/>
</span>
}
</div>
<ParticipantsTextField
ref={Fields.To}
field='to'
change={@props.onChangeParticipants}
className="composer-participant-field to-field"
draftReady={@props.draftReady}
onFocus={ => @props.onChangeFocusedField(Fields.To) }
participants={to: @props['to'], cc: @props['cc'], bcc: @props['bcc']} />
</div>
) )
if Fields.Cc in @props.enabledFields if Fields.Cc in @props.enabledFields

View file

@ -29,7 +29,7 @@ class ComposerWithWindowProps extends React.Component
render: -> render: ->
<div className="composer-full-window"> <div className="composer-full-window">
<ComposerView mode="fullwindow" draftClientId={@state.draftClientId} /> <ComposerView draftClientId={@state.draftClientId} />
</div> </div>
_showInitialErrorDialog: (msg) -> _showInitialErrorDialog: (msg) ->

View file

@ -0,0 +1,86 @@
React = require 'react'
ComposerHeaderActions = require '../lib/composer-header-actions'
Fields = require '../lib/fields'
ReactTestUtils = React.addons.TestUtils
{Actions} = require 'nylas-exports'
describe "ComposerHeaderActions", ->
makeField = (props = {}) ->
@onChangeParticipants = jasmine.createSpy("onChangeParticipants")
@onAdjustEnabledFields = jasmine.createSpy("onAdjustEnabledFields")
props.onChangeParticipants = @onChangeParticipants
props.onAdjustEnabledFields = @onAdjustEnabledFields
props.enabledFields ?= []
props.draftClientId = 'a'
@component = ReactTestUtils.renderIntoDocument(
<ComposerHeaderActions {...props} />
)
it "renders all 'show' fields when the focused field is one of the participant fields", ->
makeField.call(@, {focusedField: Fields.To})
showCc = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "show-cc")
showBcc = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "show-bcc")
showSubject = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "show-subject")
expect(showCc).toBeDefined()
expect(showBcc).toBeDefined()
expect(showSubject).toBeDefined()
it "does not render the 'show' fields when the focused field is outside the participant fields", ->
makeField.call(@, {focusedField: Fields.Subject})
showCc = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "show-cc")
showBcc = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "show-bcc")
showSubject = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "show-subject")
expect(showCc.length).toBe 0
expect(showBcc.length).toBe 0
expect(showSubject.length).toBe 0
it "hides show cc if it's enabled", ->
makeField.call(@, {focusedField: Fields.To, enabledFields: [Fields.Cc]})
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "show-cc")
expect(els.length).toBe 0
it "hides show bcc if it's enabled", ->
makeField.call(@, {focusedField: Fields.To, enabledFields: [Fields.Bcc]})
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "show-bcc")
expect(els.length).toBe 0
it "hides show subject if it's enabled", ->
makeField.call(@, {focusedField: Fields.To, enabledFields: [Fields.Subject]})
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "show-subject")
expect(els.length).toBe 0
it "renders 'popout composer' in the inline mode", ->
makeField.call(@, {focusedField: Fields.To})
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "show-popout")
expect(els.length).toBe 1
it "doesn't render 'popout composer' if in a composer window", ->
spyOn(NylasEnv, 'isComposerWindow').andReturn(true)
makeField.call(@, {focusedField: Fields.To})
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "show-popout")
expect(els.length).toBe 0
it "pops out the composer when clicked", ->
spyOn(Actions, "composePopoutDraft")
makeField.call(@, {focusedField: Fields.To})
el = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "show-popout")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(Actions.composePopoutDraft).toHaveBeenCalled()
it "shows and focuses cc when clicked", ->
makeField.call(@, {focusedField: Fields.To})
el = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "show-cc")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onAdjustEnabledFields).toHaveBeenCalledWith show: [Fields.Cc]
it "shows and focuses bcc when clicked", ->
makeField.call(@, {focusedField: Fields.To})
el = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "show-bcc")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onAdjustEnabledFields).toHaveBeenCalledWith show: [Fields.Bcc]
it "shows subject when clicked", ->
makeField.call(@, {focusedField: Fields.To})
el = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "show-subject")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onAdjustEnabledFields).toHaveBeenCalledWith show: [Fields.Subject]

View file

@ -262,19 +262,21 @@ describe "ComposerView", ->
it "focuses the body if the composer is not inline", -> it "focuses the body if the composer is not inline", ->
useDraft.call @, to: [u1], subject: "Yo" useDraft.call @, to: [u1], subject: "Yo"
makeComposer.call @, {mode: 'fullWindow'} spyOn(NylasEnv, 'isComposerWindow').andReturn(true)
makeComposer.call @
expect(@composer.state.focusedField).toBe Fields.Body expect(@composer.state.focusedField).toBe Fields.Body
it "focuses the body if the composer is inline and the thread was focused via a click", -> it "focuses the body if the composer is inline and the thread was focused via a click", ->
spyOn(FocusedContentStore, 'didFocusUsingClick').andReturn true spyOn(FocusedContentStore, 'didFocusUsingClick').andReturn true
useDraft.call @, to: [u1], subject: "Yo" useDraft.call @, to: [u1], subject: "Yo"
makeComposer.call @, {mode: 'inline'} makeComposer.call @
expect(@composer.state.focusedField).toBe Fields.Body expect(@composer.state.focusedField).toBe Fields.Body
it "does not focus any field if the composer is inline and the thread was not focused via a click", -> it "does not focus any field if the composer is inline and the thread was not focused via a click", ->
spyOn(FocusedContentStore, 'didFocusUsingClick').andReturn false spyOn(FocusedContentStore, 'didFocusUsingClick').andReturn false
spyOn(NylasEnv, 'isComposerWindow').andReturn(false)
useDraft.call @, to: [u1], subject: "Yo" useDraft.call @, to: [u1], subject: "Yo"
makeComposer.call @, {mode: 'inline'} makeComposer.call @
expect(@composer.state.focusedField).toBe null expect(@composer.state.focusedField).toBe null
describe "when deciding whether or not to enable the subject", -> describe "when deciding whether or not to enable the subject", ->

View file

@ -36,67 +36,6 @@ describe "ExpandedParticipants", ->
el = ReactTestUtils.findRenderedComponentWithType(@fields, AccountContactField) el = ReactTestUtils.findRenderedComponentWithType(@fields, AccountContactField)
expect(el).toBeDefined() expect(el).toBeDefined()
it "renders all 'show' fields", ->
makeField.call(@)
showCc = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-cc")
showBcc = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-bcc")
showSubject = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-subject")
expect(showCc).toBeDefined()
expect(showBcc).toBeDefined()
expect(showSubject).toBeDefined()
it "hides show cc if it's enabled", ->
makeField.call(@, enabledFields: [Fields.Cc])
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-cc")
expect(els.length).toBe 0
it "hides show bcc if it's enabled", ->
makeField.call(@, enabledFields: [Fields.Bcc])
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-bcc")
expect(els.length).toBe 0
it "hides show subject if it's enabled", ->
makeField.call(@, enabledFields: [Fields.Subject])
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-subject")
expect(els.length).toBe 0
it "renders popout composer in the inline mode", ->
makeField.call(@, mode: "inline")
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-popout")
expect(els.length).toBe 1
it "doesn't render popout composer in the fullwindow mode", ->
makeField.call(@, mode: "fullwindow")
els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, "show-popout")
expect(els.length).toBe 0
it "pops out the composer when clicked", ->
spyOn(Actions, "composePopoutDraft")
onPopoutComposer = jasmine.createSpy('onPopoutComposer')
makeField.call(@, mode: "inline", onPopoutComposer: onPopoutComposer)
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-popout")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(onPopoutComposer).toHaveBeenCalled()
expect(onPopoutComposer.calls.length).toBe 1
it "shows and focuses cc when clicked", ->
makeField.call(@)
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-cc")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onAdjustEnabledFields).toHaveBeenCalledWith show: [Fields.Cc]
it "shows and focuses bcc when clicked", ->
makeField.call(@)
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-bcc")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onAdjustEnabledFields).toHaveBeenCalledWith show: [Fields.Bcc]
it "shows subject when clicked", ->
makeField.call(@)
el = ReactTestUtils.findRenderedDOMComponentWithClass(@fields, "show-subject")
ReactTestUtils.Simulate.click(React.findDOMNode(el))
expect(@onAdjustEnabledFields).toHaveBeenCalledWith show: [Fields.Subject]
it "empties cc and focuses on to field", -> it "empties cc and focuses on to field", ->
makeField.call(@, enabledFields: [Fields.Cc, Fields.Bcc, Fields.Subject]) makeField.call(@, enabledFields: [Fields.Cc, Fields.Bcc, Fields.Subject])
@fields.refs[Fields.Cc].props.onEmptied() @fields.refs[Fields.Cc].props.onEmptied()

View file

@ -127,19 +127,7 @@ body.platform-win32 {
top: -3px; top: -3px;
} }
.header-action { .composer-header-actions {
color: @text-color-very-subtle;
img.content-mask { background-color: @text-color-very-subtle; }
font-size: @font-size-small;
padding-left: 1em;
&:hover {
color: @text-color-link;
img.content-mask { background-color: @text-color-link; }
cursor: default;
}
}
.composer-participant-actions {
position: relative; position: relative;
float: right; float: right;
z-index: 2; z-index: 2;
@ -147,6 +135,18 @@ body.platform-win32 {
padding-right: @spacing-standard + @spacing-half; padding-right: @spacing-standard + @spacing-half;
padding-left: @spacing-standard; padding-left: @spacing-standard;
padding-top: 12px; padding-top: 12px;
.action {
color: @text-color-very-subtle;
img.content-mask { background-color: @text-color-very-subtle; }
font-size: @font-size-small;
padding-left: 1em;
&:hover {
color: @text-color-link;
img.content-mask { background-color: @text-color-link; }
cursor: default;
}
}
} }
input, textarea { input, textarea {

View file

@ -19,6 +19,10 @@ class DestroyDraftTask extends BaseDraftTask
@refreshDraftReference().then => @refreshDraftReference().then =>
DatabaseStore.inTransaction (t) => DatabaseStore.inTransaction (t) =>
t.unpersistModel(@draft) t.unpersistModel(@draft)
.catch (err) =>
if err instanceof BaseDraftTask.DraftNotFoundError
return Promise.resolve()
Promsie.reject(err)
performRemote: -> performRemote: ->
# We don't need to do anything if we weren't able to find the draft # We don't need to do anything if we weren't able to find the draft