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

View file

@ -35,9 +35,6 @@ class ExpandedParticipants extends React.Component
# The account to which the current draft belongs
accounts: React.PropTypes.array
# Either "fullwindow" or "inline"
mode: React.PropTypes.string
# The field that should be focused
focusedField: React.PropTypes.string
@ -74,8 +71,7 @@ class ExpandedParticipants extends React.Component
@_applyFocusedField()
render: ->
<div className="expanded-participants"
ref="participantWrap">
<div className="expanded-participants" ref="participantWrap">
{@_renderFields()}
</div>
@ -92,43 +88,15 @@ class ExpandedParticipants extends React.Component
# If they're hidden, shift-tab between fields breaks.
fields = []
fields.push(
<div key="to">
<div className="composer-participant-actions">
{if Fields.Cc not in @props.enabledFields
<span className="header-action show-cc"
onClick={ => @props.onAdjustEnabledFields(show: [Fields.Cc]) }>Cc</span>
}
{ if Fields.Bcc not in @props.enabledFields
<span className="header-action show-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>
<ParticipantsTextField
ref={Fields.To}
key="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']} />
)
if Fields.Cc in @props.enabledFields

View file

@ -29,7 +29,7 @@ class ComposerWithWindowProps extends React.Component
render: ->
<div className="composer-full-window">
<ComposerView mode="fullwindow" draftClientId={@state.draftClientId} />
<ComposerView draftClientId={@state.draftClientId} />
</div>
_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", ->
useDraft.call @, to: [u1], subject: "Yo"
makeComposer.call @, {mode: 'fullWindow'}
spyOn(NylasEnv, 'isComposerWindow').andReturn(true)
makeComposer.call @
expect(@composer.state.focusedField).toBe Fields.Body
it "focuses the body if the composer is inline and the thread was focused via a click", ->
spyOn(FocusedContentStore, 'didFocusUsingClick').andReturn true
useDraft.call @, to: [u1], subject: "Yo"
makeComposer.call @, {mode: 'inline'}
makeComposer.call @
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", ->
spyOn(FocusedContentStore, 'didFocusUsingClick').andReturn false
spyOn(NylasEnv, 'isComposerWindow').andReturn(false)
useDraft.call @, to: [u1], subject: "Yo"
makeComposer.call @, {mode: 'inline'}
makeComposer.call @
expect(@composer.state.focusedField).toBe null
describe "when deciding whether or not to enable the subject", ->

View file

@ -36,67 +36,6 @@ describe "ExpandedParticipants", ->
el = ReactTestUtils.findRenderedComponentWithType(@fields, AccountContactField)
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", ->
makeField.call(@, enabledFields: [Fields.Cc, Fields.Bcc, Fields.Subject])
@fields.refs[Fields.Cc].props.onEmptied()

View file

@ -127,19 +127,7 @@ body.platform-win32 {
top: -3px;
}
.header-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;
}
}
.composer-participant-actions {
.composer-header-actions {
position: relative;
float: right;
z-index: 2;
@ -147,6 +135,18 @@ body.platform-win32 {
padding-right: @spacing-standard + @spacing-half;
padding-left: @spacing-standard;
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 {

View file

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