mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
feat(send): Send and Archive
Summary: Send and Archive plus a new setting. Test Plan: new tests Reviewers: bengotow, juan Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2446
This commit is contained in:
parent
ac34f4410b
commit
ecbadaf01e
19 changed files with 647 additions and 75 deletions
|
@ -26,6 +26,7 @@ FileUpload = require './file-upload'
|
|||
ImageFileUpload = require './image-file-upload'
|
||||
|
||||
ComposerEditor = require './composer-editor'
|
||||
SendActionButton = require './send-action-button'
|
||||
ExpandedParticipants = require './expanded-participants'
|
||||
CollapsedParticipants = require './collapsed-participants'
|
||||
|
||||
|
@ -111,7 +112,7 @@ class ComposerView extends React.Component
|
|||
@_applyFieldFocus()
|
||||
|
||||
_keymapHandlers: ->
|
||||
'composer:send-message': => @_sendDraft()
|
||||
'composer:send-message': => @_onPrimarySend()
|
||||
'composer:delete-empty-draft': => @_deleteDraftIfEmpty()
|
||||
'composer:show-and-focus-bcc': =>
|
||||
@_onAdjustEnabledFields(show: [Fields.Bcc])
|
||||
|
@ -468,9 +469,9 @@ class ComposerView extends React.Component
|
|||
|
||||
<div style={order: 0, flex: 1} />
|
||||
|
||||
<button className="btn btn-toolbar btn-emphasis btn-text btn-send" style={order: -100}
|
||||
ref="sendButton"
|
||||
onClick={@_sendDraft}><RetinaImg name="icon-composer-send.png" mode={RetinaImg.Mode.ContentIsMask} /><span className="text">Send</span></button>
|
||||
<SendActionButton draft={@_proxy?.draft()}
|
||||
ref="sendActionButton"
|
||||
isValidDraft={@_isValidDraft} />
|
||||
|
||||
</InjectedComponentSet>
|
||||
|
||||
|
@ -687,14 +688,14 @@ class ComposerView extends React.Component
|
|||
|
||||
@_saveToHistory(selections) unless source.fromUndoManager
|
||||
|
||||
_sendDraft: (options = {}) =>
|
||||
return unless @_proxy
|
||||
_isValidDraft: (options = {}) =>
|
||||
return false unless @_proxy
|
||||
|
||||
# We need to check the `DraftStore` because the `DraftStore` is
|
||||
# immediately and synchronously updated as soon as this function
|
||||
# fires. Since `setState` is asynchronous, if we used that as our only
|
||||
# check, then we might get a false reading.
|
||||
return if DraftStore.isSendingDraft(@props.draftClientId)
|
||||
return false if DraftStore.isSendingDraft(@props.draftClientId)
|
||||
|
||||
draft = @_proxy.draft()
|
||||
remote = require('remote')
|
||||
|
@ -714,7 +715,7 @@ class ComposerView extends React.Component
|
|||
message: 'Cannot Send',
|
||||
detail: dealbreaker
|
||||
})
|
||||
return
|
||||
return false
|
||||
|
||||
bodyIsEmpty = draft.body is @_proxy.draftPristineBody()
|
||||
forwarded = Utils.isForwardedMessage(draft)
|
||||
|
@ -744,10 +745,13 @@ class ComposerView extends React.Component
|
|||
detail: "Send #{warnings.join(' and ')}?"
|
||||
})
|
||||
if response is 0 # response is button array index
|
||||
@_sendDraft({force: true})
|
||||
return
|
||||
return @_isValidDraft({force: true})
|
||||
else return false
|
||||
|
||||
Actions.sendDraft(@props.draftClientId)
|
||||
return true
|
||||
|
||||
_onPrimarySend: ->
|
||||
@refs["sendActionButton"].primaryClick()
|
||||
|
||||
_mentionsAttachment: (body) =>
|
||||
body = QuotedHTMLTransformer.removeQuotedHTML(body.toLowerCase().trim())
|
||||
|
|
145
internal_packages/composer/lib/send-action-button.cjsx
Normal file
145
internal_packages/composer/lib/send-action-button.cjsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
_ = require 'underscore'
|
||||
_str = require 'underscore.string'
|
||||
{React, Actions, ExtensionRegistry} = require 'nylas-exports'
|
||||
{Menu, RetinaImg, ButtonDropdown} = require 'nylas-component-kit'
|
||||
|
||||
class SendActionButton extends React.Component
|
||||
@displayName: "SendActionButton"
|
||||
|
||||
@propTypes:
|
||||
draft: React.PropTypes.object
|
||||
isValidDraft: React.PropTypes.func
|
||||
|
||||
@CONFIG_KEY: "core.sending.defaultSendType"
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
actionConfigs: @_actionConfigs(@props)
|
||||
selectedSendType: NylasEnv.config.get(SendActionButton.CONFIG_KEY) ? @_defaultActionConfig().configKey
|
||||
|
||||
componentDidMount: ->
|
||||
@unsub = ExtensionRegistry.Composer.listen(@_onExtensionsChanged)
|
||||
|
||||
componentWillReceiveProps: (newProps) ->
|
||||
@setState actionConfigs: @_actionConfigs(newProps)
|
||||
|
||||
componentWillUnmount: ->
|
||||
@unsub()
|
||||
|
||||
primaryClick: => @_onPrimaryClick()
|
||||
|
||||
_configKeyFromTitle: (title) ->
|
||||
return _str.dasherize(title.toLowerCase())
|
||||
|
||||
_onExtensionsChanged: =>
|
||||
@setState actionConfigs: @_actionConfigs(@props)
|
||||
|
||||
_defaultActionConfig: ->
|
||||
title: "Send"
|
||||
iconUrl: null
|
||||
onSend: ({draft}) -> Actions.sendDraft(draft.clientId)
|
||||
configKey: "send"
|
||||
|
||||
_actionConfigs: (props) ->
|
||||
return [] unless props.draft
|
||||
actionConfigs = [@_defaultActionConfig()]
|
||||
|
||||
for extension in ExtensionRegistry.Composer.extensions()
|
||||
try
|
||||
actionConfig = extension.sendActionConfig?({draft: props.draft})
|
||||
if actionConfig
|
||||
@_verifyConfig(actionConfig, extension)
|
||||
actionConfig.configKey = @_configKeyFromTitle(actionConfig.title)
|
||||
actionConfigs.push(actionConfig)
|
||||
catch err
|
||||
NylasEnv.emitError(err)
|
||||
|
||||
return actionConfigs
|
||||
|
||||
_verifyConfig: (config={}, extension) ->
|
||||
name = extension.name
|
||||
if not _.isString(config.title)
|
||||
throw new Error("#{name}.sendActionConfig must return a string `title`")
|
||||
|
||||
if not _.isFunction(config.onSend)
|
||||
throw new Error("#{name}.sendActionConfig must return a `onSend` function that will be called when the action is selected")
|
||||
|
||||
return true
|
||||
|
||||
render: ->
|
||||
return false if not @props.draft
|
||||
if @state.actionConfigs.length is 1
|
||||
@_renderSingleDefaultButton()
|
||||
else
|
||||
@_renderSendDropdown()
|
||||
|
||||
_onPrimaryClick: =>
|
||||
actionConfigs = @_orderedActionConfigs()
|
||||
@_sendWithAction(actionConfigs[0].onSend)
|
||||
|
||||
_renderSingleDefaultButton: ->
|
||||
classes = "btn btn-toolbar btn-normal btn-emphasis btn-text btn-send"
|
||||
iconUrl = @state.actionConfigs[0].iconUrl
|
||||
<button className={classes}
|
||||
style={order: -100}
|
||||
onClick={@_onPrimaryClick}>{@_sendContent(iconUrl)}</button>
|
||||
|
||||
_renderSendDropdown: ->
|
||||
actionConfigs = @_orderedActionConfigs()
|
||||
<ButtonDropdown
|
||||
className={"btn-send dropdown-btn-emphasis dropdown-btn-text"}
|
||||
style={order: -100}
|
||||
primaryItem={@_sendContent(actionConfigs[0].iconUrl)}
|
||||
primaryTitle={actionConfigs[0].title}
|
||||
primaryClick={@_onPrimaryClick}
|
||||
closeOnMenuClick={true}
|
||||
menu={@_dropdownMenu(actionConfigs[1..-1])}/>
|
||||
|
||||
_orderedActionConfigs: ->
|
||||
configKeys = _.pluck(@state.actionConfigs, "configKey")
|
||||
if @state.selectedSendType not in configKeys
|
||||
selectedSendType = @_defaultActionConfig().configKey
|
||||
else
|
||||
selectedSendType = @state.selectedSendType
|
||||
|
||||
primary = _.findWhere(@state.actionConfigs, configKey: selectedSendType)
|
||||
rest = _.reject @state.actionConfigs, (config) ->
|
||||
config.configKey is selectedSendType
|
||||
|
||||
return [primary].concat(rest)
|
||||
|
||||
_sendWithAction: (onSend) ->
|
||||
isValidDraft = @props.isValidDraft()
|
||||
if isValidDraft
|
||||
try
|
||||
onSend({draft: @props.draft})
|
||||
catch err
|
||||
NylasEnv.emitError(err)
|
||||
|
||||
_dropdownMenu: (actionConfigs) ->
|
||||
<Menu items={actionConfigs}
|
||||
itemKey={ (actionConfig) -> actionConfig.configKey }
|
||||
itemContent={ (actionConfig) => @_sendContent(actionConfig.iconUrl) }
|
||||
onSelect={@_menuItemSelect}
|
||||
/>
|
||||
|
||||
_menuItemSelect: (actionConfig) =>
|
||||
@setState selectedSendType: actionConfig.configKey
|
||||
|
||||
_sendContent: (iconUrl) ->
|
||||
sendIcon = "icon-composer-send.png"
|
||||
|
||||
if iconUrl
|
||||
plusHTML = <span> + </span>
|
||||
additionalImg = <RetinaImg url={iconUrl}
|
||||
mode={RetinaImg.Mode.ContentIsMask} />
|
||||
else
|
||||
plusHTML = ""
|
||||
additionalImg = ""
|
||||
|
||||
<span>
|
||||
<RetinaImg name={sendIcon} mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<span className="text">Send{plusHTML}</span>{additionalImg}
|
||||
</span>
|
||||
|
||||
module.exports = SendActionButton
|
|
@ -110,6 +110,7 @@ describe "ComposerView", ->
|
|||
DRAFT_CLIENT_ID = "local-123"
|
||||
useDraft = (draftAttributes={}) ->
|
||||
@draft = new Message _.extend({draft: true, body: ""}, draftAttributes)
|
||||
@draft.clientId = DRAFT_CLIENT_ID
|
||||
draft = @draft
|
||||
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
|
||||
@proxy = proxy
|
||||
|
@ -426,8 +427,8 @@ describe "ComposerView", ->
|
|||
it "shows an error if there are no recipients", ->
|
||||
useDraft.call @, subject: "no recipients"
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe false
|
||||
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.detail).toEqual("You need to provide one or more recipients before sending the message.")
|
||||
|
@ -438,8 +439,8 @@ describe "ComposerView", ->
|
|||
subject: 'hello world!'
|
||||
to: [new Contact(email: 'lol', name: 'lol')]
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe false
|
||||
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.detail).toEqual("lol is not a valid email address - please remove or edit it before sending.")
|
||||
|
@ -457,8 +458,8 @@ describe "ComposerView", ->
|
|||
|
||||
spyOn(@composer._proxy, 'draftPristineBody').andCallFake -> pristineBody
|
||||
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe false
|
||||
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']
|
||||
|
@ -469,8 +470,8 @@ describe "ComposerView", ->
|
|||
subject: "Fwd: Hello World"
|
||||
body: "<br><br><blockquote class='gmail_quote'>This is my quoted text!</blockquote>"
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe true
|
||||
|
||||
it "does not warn if the user has attached a file", ->
|
||||
useDraft.call @,
|
||||
|
@ -479,38 +480,40 @@ describe "ComposerView", ->
|
|||
body: ""
|
||||
files: [f1]
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe true
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
it "shows a warning if there's no subject", ->
|
||||
useDraft.call @, to: [u1], subject: ""
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe false
|
||||
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']
|
||||
|
||||
it "doesn't show a warning if requirements are satisfied", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe true
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
describe "Checking for attachments", ->
|
||||
warn = (body) ->
|
||||
useDraft.call @, subject: "Subject", to: [u1], body: body
|
||||
makeComposer.call(@); @composer._sendDraft()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
makeComposer.call(@)
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe false
|
||||
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']
|
||||
|
||||
noWarn = (body) ->
|
||||
useDraft.call @, subject: "Subject", to: [u1], body: body
|
||||
makeComposer.call(@); @composer._sendDraft()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
makeComposer.call(@)
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe true
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
it "warns", -> warn.call(@, "Check out the attached file")
|
||||
|
@ -528,31 +531,32 @@ describe "ComposerView", ->
|
|||
to: [u1]
|
||||
body: "Check out attached file"
|
||||
files: [f1]
|
||||
makeComposer.call(@); @composer._sendDraft()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
makeComposer.call(@)
|
||||
status = @composer._isValidDraft()
|
||||
expect(status).toBe true
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
it "bypasses the warning if force bit is set", ->
|
||||
useDraft.call @, to: [u1], subject: ""
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft(force: true)
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
status = @composer._isValidDraft(force: true)
|
||||
expect(status).toBe true
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
it "sends when you click the send button", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
sendBtn = @composer.refs.sendActionButton
|
||||
sendBtn.primaryClick()
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID)
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
it "doesn't send twice if you double click", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
sendBtn = @composer.refs.sendActionButton
|
||||
sendBtn.primaryClick()
|
||||
@isSending = true
|
||||
DraftStore.trigger()
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
sendBtn.primaryClick()
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID)
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
|
@ -764,9 +768,12 @@ describe "ComposerView", ->
|
|||
spyOn(Actions, "sendDraft").andCallThrough()
|
||||
useFullDraft.call(@)
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
firstStatus = @composer._isValidDraft()
|
||||
expect(firstStatus).toBe true
|
||||
Actions.sendDraft(DRAFT_CLIENT_ID)
|
||||
secondStatus = @composer._isValidDraft()
|
||||
expect(secondStatus).toBe false
|
||||
|
||||
it "doesn't send twice in the main window", ->
|
||||
spyOn(Actions, "queueTask")
|
||||
|
@ -774,6 +781,9 @@ describe "ComposerView", ->
|
|||
spyOn(NylasEnv, "isMainWindow").andReturn true
|
||||
useFullDraft.call(@)
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
firstStatus = @composer._isValidDraft()
|
||||
expect(firstStatus).toBe true
|
||||
Actions.sendDraft(DRAFT_CLIENT_ID)
|
||||
secondStatus = @composer._isValidDraft()
|
||||
expect(secondStatus).toBe false
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ Fields = require '../lib/fields'
|
|||
Composer = require "../lib/composer-view"
|
||||
ComposerEditor = require '../lib/composer-editor'
|
||||
|
||||
{DraftStore, ComponentRegistry} = require 'nylas-exports'
|
||||
{Message, DraftStore, ComponentRegistry} = require 'nylas-exports'
|
||||
|
||||
describe "Composer Quoted Text", ->
|
||||
beforeEach ->
|
||||
|
@ -27,8 +27,12 @@ describe "Composer Quoted Text", ->
|
|||
|
||||
spyOn(Composer.prototype, "_prepareForDraft")
|
||||
|
||||
@draft = new Message(draft: true, clientId: "client-123")
|
||||
|
||||
@composer = ReactTestUtils.renderIntoDocument(<Composer draftClientId="unused"/>)
|
||||
@composer._proxy = trigger: ->
|
||||
@composer._proxy =
|
||||
trigger: ->
|
||||
draft: => @draft
|
||||
spyOn(@composer, "_addToProxy")
|
||||
|
||||
spyOn(@composer, "_setupSession")
|
||||
|
|
191
internal_packages/composer/spec/send-actions-spec.cjsx
Normal file
191
internal_packages/composer/spec/send-actions-spec.cjsx
Normal file
|
@ -0,0 +1,191 @@
|
|||
React = require "react/addons"
|
||||
ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
SendActionButton = require '../lib/send-action-button'
|
||||
{Actions, Message, ComposerExtension, ExtensionRegistry} = require 'nylas-exports'
|
||||
{ButtonDropdown, RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class NAExtension extends ComposerExtension
|
||||
|
||||
class GoodExtension extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) ->
|
||||
title: "Good Extension"
|
||||
content: -> <div className="btn-good"></div>
|
||||
onSend: ->
|
||||
|
||||
class SecondExtension extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) ->
|
||||
title: "Second Extension"
|
||||
content: -> <div className="btn-second"></div>
|
||||
onSend: ->
|
||||
|
||||
class NullExtension extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) -> null
|
||||
|
||||
isValidDraft = null
|
||||
|
||||
describe "SendActionButton", ->
|
||||
render = (draft, valid=true) ->
|
||||
isValidDraft = jasmine.createSpy("isValidDraft").andReturn(valid)
|
||||
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<SendActionButton draft={draft} isValidDraft={isValidDraft} />
|
||||
)
|
||||
|
||||
beforeEach ->
|
||||
spyOn(NylasEnv, "emitError")
|
||||
spyOn(Actions, "sendDraft")
|
||||
@clientId = "client-23"
|
||||
@draft = new Message(clientId: @clientId, draft: true)
|
||||
|
||||
it "renders without error", ->
|
||||
@sendActionButton = render(@draft)
|
||||
expect(ReactTestUtils.isCompositeComponentWithType @sendActionButton, SendActionButton).toBe true
|
||||
|
||||
it "is a single button when there are no extensions", ->
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn []
|
||||
@sendActionButton = render(@draft)
|
||||
dropdowns = ReactTestUtils.scryRenderedComponentsWithType(@sendActionButton, ButtonDropdown)
|
||||
buttons = ReactTestUtils.scryRenderedDOMComponentsWithTag(@sendActionButton, "button")
|
||||
expect(buttons.length).toBe 1
|
||||
expect(dropdowns.length).toBe 0
|
||||
|
||||
it "is a dropdown when there's another valid extension", ->
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [GoodExtension]
|
||||
@sendActionButton = render(@draft)
|
||||
dropdowns = ReactTestUtils.scryRenderedComponentsWithType(@sendActionButton, ButtonDropdown)
|
||||
buttons = ReactTestUtils.scryRenderedDOMComponentsWithTag(@sendActionButton, "button")
|
||||
expect(buttons.length).toBe 0
|
||||
expect(dropdowns.length).toBe 1
|
||||
|
||||
it "has the correct primary item", ->
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [GoodExtension, SecondExtension]
|
||||
@sendActionButton = render(@draft)
|
||||
@sendActionButton.setState(selectedSendType: 'second-extension')
|
||||
dropdown = ReactTestUtils.findRenderedComponentWithType(@sendActionButton, ButtonDropdown)
|
||||
expect(dropdown.props.primaryTitle).toBe "Second Extension"
|
||||
|
||||
it "falls back to a default if the primary item can't be found", ->
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [GoodExtension, SecondExtension]
|
||||
@sendActionButton = render(@draft)
|
||||
@sendActionButton.setState(selectedSendType: 'does-not-exist')
|
||||
dropdown = ReactTestUtils.findRenderedComponentWithType(@sendActionButton, ButtonDropdown)
|
||||
expect(dropdown.props.primaryTitle).toBe "Send"
|
||||
|
||||
it "is a single button when a valid extension returns null", ->
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [NullExtension]
|
||||
@sendActionButton = render(@draft)
|
||||
dropdowns = ReactTestUtils.scryRenderedComponentsWithType(@sendActionButton, ButtonDropdown)
|
||||
buttons = ReactTestUtils.scryRenderedDOMComponentsWithTag(@sendActionButton, "button")
|
||||
expect(buttons.length).toBe 1
|
||||
expect(dropdowns.length).toBe 0
|
||||
|
||||
it "still renders but catches when an extension is missing a title", ->
|
||||
class NoTitle extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) ->
|
||||
title: null
|
||||
iconUrl: "nylas://foo/bar/baz"
|
||||
onSend: ->
|
||||
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [NoTitle]
|
||||
@sendActionButton = render(@draft)
|
||||
dropdowns = ReactTestUtils.scryRenderedComponentsWithType(@sendActionButton, ButtonDropdown)
|
||||
buttons = ReactTestUtils.scryRenderedDOMComponentsWithTag(@sendActionButton, "button")
|
||||
expect(buttons.length).toBe 1
|
||||
expect(dropdowns.length).toBe 0
|
||||
expect(NylasEnv.emitError).toHaveBeenCalled()
|
||||
expect(NylasEnv.emitError.calls[0].args[0].message).toMatch /title/
|
||||
|
||||
it "still renders with a null iconUrl and doesn't show the image", ->
|
||||
class NoIconUrl extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) ->
|
||||
title: "some title"
|
||||
iconUrl: null
|
||||
onSend: ->
|
||||
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [NoIconUrl]
|
||||
@sendActionButton = render(@draft)
|
||||
@sendActionButton.setState(selectedSendType: 'some-title')
|
||||
dropdowns = ReactTestUtils.scryRenderedComponentsWithType(@sendActionButton, ButtonDropdown)
|
||||
icons = ReactTestUtils.scryRenderedComponentsWithType(@sendActionButton, RetinaImg)
|
||||
buttons = ReactTestUtils.scryRenderedDOMComponentsWithTag(@sendActionButton, "button")
|
||||
expect(buttons.length).toBe 0 # It's a dropdown instead
|
||||
expect(dropdowns.length).toBe 1
|
||||
expect(icons.length).toBe 3
|
||||
|
||||
it "still renders but catches when an extension is missing an onSend", ->
|
||||
class NoClick extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) ->
|
||||
title: "some title"
|
||||
iconUrl: "nylas://foo/bar/baz"
|
||||
onSend: null
|
||||
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [NoClick]
|
||||
@sendActionButton = render(@draft)
|
||||
dropdowns = ReactTestUtils.scryRenderedComponentsWithType(@sendActionButton, ButtonDropdown)
|
||||
buttons = ReactTestUtils.scryRenderedDOMComponentsWithTag(@sendActionButton, "button")
|
||||
expect(buttons.length).toBe 1
|
||||
expect(dropdowns.length).toBe 0
|
||||
expect(NylasEnv.emitError).toHaveBeenCalled()
|
||||
expect(NylasEnv.emitError.calls[0].args[0].message).toMatch /onSend/
|
||||
|
||||
it "sends a draft by default", ->
|
||||
@sendActionButton = render(@draft)
|
||||
button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(@sendActionButton, "button"))
|
||||
ReactTestUtils.Simulate.click(button)
|
||||
expect(isValidDraft).toHaveBeenCalled()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(Actions.sendDraft.calls[0].args[0]).toBe @draft.clientId
|
||||
|
||||
it "doesn't send a draft if the isValidDraft fails", ->
|
||||
@sendActionButton = render(@draft, false)
|
||||
button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(@sendActionButton, "button"))
|
||||
ReactTestUtils.Simulate.click(button)
|
||||
expect(isValidDraft).toHaveBeenCalled()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
|
||||
it "does the primaryClick action of the extension", ->
|
||||
clicked = false
|
||||
class Click extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) ->
|
||||
title: "click"
|
||||
iconUrl: "nylas://foo/bar/baz"
|
||||
onSend: -> clicked = "onSend fired"
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [Click]
|
||||
|
||||
@sendActionButton = render(@draft)
|
||||
@sendActionButton.setState(selectedSendType: 'click')
|
||||
|
||||
button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(@sendActionButton, "primary-item"))
|
||||
ReactTestUtils.Simulate.click(button)
|
||||
expect(clicked).toBe "onSend fired"
|
||||
|
||||
it "catches any errors in an extension's primaryClick method", ->
|
||||
clicked = false
|
||||
class Click extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) ->
|
||||
title: "click"
|
||||
iconUrl: "nylas://foo/bar/baz"
|
||||
onSend: -> throw new Error("BOO")
|
||||
spyOn(ExtensionRegistry.Composer, "extensions").andReturn [Click]
|
||||
|
||||
@sendActionButton = render(@draft)
|
||||
@sendActionButton.setState(selectedSendType: 'click')
|
||||
|
||||
button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(@sendActionButton, "primary-item"))
|
||||
ReactTestUtils.Simulate.click(button)
|
||||
expect(clicked).toBe false
|
||||
expect(NylasEnv.emitError).toHaveBeenCalled()
|
||||
expect(NylasEnv.emitError.calls[0].args[0].message).toMatch /BOO/
|
||||
|
||||
it "initializes with the correct config item", ->
|
||||
spyOn(NylasEnv.config, "get").andReturn "test-state"
|
||||
@sendActionButton = render(@draft)
|
||||
expect(@sendActionButton.state.selectedSendType).toBe "test-state"
|
||||
|
||||
it "initializes with the default key", ->
|
||||
spyOn(NylasEnv.config, "get").andReturn null
|
||||
@sendActionButton = render(@draft)
|
||||
expect(@sendActionButton.state.selectedSendType).toBe "send"
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
11
internal_packages/send-and-archive/lib/main.coffee
Normal file
11
internal_packages/send-and-archive/lib/main.coffee
Normal file
|
@ -0,0 +1,11 @@
|
|||
{ExtensionRegistry} = require 'nylas-exports'
|
||||
SendAndArchiveExtension = require './send-and-archive-extension'
|
||||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
ExtensionRegistry.Composer.register(SendAndArchiveExtension)
|
||||
|
||||
deactivate: ->
|
||||
ExtensionRegistry.Composer.unregister(SendAndArchiveExtension)
|
||||
|
||||
serialize: -> @state
|
|
@ -0,0 +1,26 @@
|
|||
{React,
|
||||
Actions,
|
||||
TaskFactory,
|
||||
ComposerExtension,
|
||||
FocusedMailViewStore} = require 'nylas-exports'
|
||||
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class SendAndArchiveExtension extends ComposerExtension
|
||||
@sendActionConfig: ({draft}) ->
|
||||
if draft.threadId
|
||||
return {
|
||||
title: "Send and Archive"
|
||||
iconUrl: "nylas://send-and-archive/images/composer-archive@2x.png"
|
||||
onSend: @_sendAndArchive
|
||||
}
|
||||
else return null
|
||||
|
||||
@_sendAndArchive: ({draft}) ->
|
||||
Actions.sendDraft(draft.clientId)
|
||||
archiveTask = TaskFactory.taskForArchiving
|
||||
threads: [draft.threadId]
|
||||
fromView: FocusedMailViewStore.mailView()
|
||||
Actions.queueTask(archiveTask)
|
||||
|
||||
module.exports = SendAndArchiveExtension
|
17
internal_packages/send-and-archive/package.json
Executable file
17
internal_packages/send-and-archive/package.json
Executable file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "composer-signature",
|
||||
"version": "0.1.0",
|
||||
"main": "./lib/main",
|
||||
"description": "A small extension to the draft store that implements signatures",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"nylas": "*"
|
||||
},
|
||||
"windowTypes": {
|
||||
"default": true,
|
||||
"composer": true
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
describe "SendAndArchive", ->
|
|
@ -0,0 +1,2 @@
|
|||
.send-and-archive {
|
||||
}
|
|
@ -9,9 +9,11 @@
|
|||
DatabaseStore,
|
||||
SoundRegistry,
|
||||
SendDraftTask,
|
||||
ChangeMailTask,
|
||||
DestroyDraftTask,
|
||||
ComposerExtension,
|
||||
ExtensionRegistry,
|
||||
FocusedContentStore,
|
||||
DatabaseTransaction,
|
||||
SanitizeTransformer,
|
||||
InlineStyleTransformer} = require 'nylas-exports'
|
||||
|
@ -657,7 +659,7 @@ describe "DraftStore", ->
|
|||
DraftStore._draftSessions = {}
|
||||
DraftStore._draftsSending = {}
|
||||
@forceCommit = false
|
||||
msg = new Message(clientId: draftClientId)
|
||||
msg = new Message(clientId: draftClientId, threadId: "thread-123", replyToMessageId: "message-123")
|
||||
proxy =
|
||||
prepare: -> Promise.resolve(proxy)
|
||||
teardown: ->
|
||||
|
@ -744,6 +746,28 @@ describe "DraftStore", ->
|
|||
runs ->
|
||||
expect(@forceCommit).toBe true
|
||||
|
||||
it "includes the threadId", ->
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
DraftStore._doneWithSession.calls.length > 0
|
||||
runs ->
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task instanceof SendDraftTask).toBe true
|
||||
expect(task.threadId).toBe "thread-123"
|
||||
|
||||
it "includes the replyToMessageId", ->
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
DraftStore._doneWithSession.calls.length > 0
|
||||
runs ->
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task instanceof SendDraftTask).toBe true
|
||||
expect(task.replyToMessageId).toBe "message-123"
|
||||
|
||||
it "queues a SendDraftTask", ->
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
|
@ -754,20 +778,6 @@ describe "DraftStore", ->
|
|||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task instanceof SendDraftTask).toBe true
|
||||
expect(task.draftClientId).toBe draftClientId
|
||||
expect(task.fromPopout).toBe false
|
||||
|
||||
it "queues a SendDraftTask with popout info", ->
|
||||
spyOn(NylasEnv, "getWindowType").andReturn "composer"
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn false
|
||||
spyOn(NylasEnv, "close")
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
DraftStore._doneWithSession.calls.length > 0
|
||||
runs ->
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task.fromPopout).toBe true
|
||||
|
||||
it "resets the sending state if there's an error", ->
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn false
|
||||
|
@ -778,17 +788,44 @@ describe "DraftStore", ->
|
|||
|
||||
it "displays a popup in the main window if there's an error", ->
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn true
|
||||
spyOn(FocusedContentStore, "focused").andReturn(id: "t1")
|
||||
remote = require('remote')
|
||||
dialog = remote.require('dialog')
|
||||
spyOn(dialog, "showMessageBox")
|
||||
spyOn(Actions, "composePopoutDraft")
|
||||
DraftStore._draftsSending[draftClientId] = true
|
||||
Actions.draftSendingFailed({errorMessage: "boohoo", draftClientId})
|
||||
Actions.draftSendingFailed({threadId: 't1', errorMessage: "boohoo", draftClientId})
|
||||
advanceClock(200)
|
||||
expect(DraftStore.isSendingDraft(draftClientId)).toBe false
|
||||
expect(DraftStore.trigger).toHaveBeenCalledWith(draftClientId)
|
||||
expect(dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.detail).toEqual("boohoo")
|
||||
expect(Actions.composePopoutDraft).not.toHaveBeenCalled
|
||||
|
||||
it "re-opens the draft if you're not looking at the thread", ->
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn true
|
||||
spyOn(FocusedContentStore, "focused").andReturn(id: "t1")
|
||||
spyOn(Actions, "composePopoutDraft")
|
||||
DraftStore._draftsSending[draftClientId] = true
|
||||
Actions.draftSendingFailed({threadId: 't2', errorMessage: "boohoo", draftClientId})
|
||||
advanceClock(200)
|
||||
expect(Actions.composePopoutDraft).toHaveBeenCalled
|
||||
call = Actions.composePopoutDraft.calls[0]
|
||||
expect(call.args[0]).toBe draftClientId
|
||||
expect(call.args[1]).toEqual {errorMessage: "boohoo"}
|
||||
|
||||
it "re-opens the draft if there is no thread id", ->
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn true
|
||||
spyOn(Actions, "composePopoutDraft")
|
||||
DraftStore._draftsSending[draftClientId] = true
|
||||
spyOn(FocusedContentStore, "focused").andReturn(null)
|
||||
Actions.draftSendingFailed({errorMessage: "boohoo", draftClientId})
|
||||
advanceClock(200)
|
||||
expect(Actions.composePopoutDraft).toHaveBeenCalled
|
||||
call = Actions.composePopoutDraft.calls[0]
|
||||
expect(call.args[0]).toBe draftClientId
|
||||
expect(call.args[1]).toEqual {errorMessage: "boohoo"}
|
||||
|
||||
describe "session teardown", ->
|
||||
beforeEach ->
|
||||
|
|
|
@ -74,6 +74,8 @@ describe "SendDraftTask", ->
|
|||
expect(task.backupDraft.clientId).toBe "local-123"
|
||||
expect(task.backupDraft.serverId).toBe "server-123"
|
||||
expect(task.backupDraft).not.toBe draft # It's a clone
|
||||
expect(task.replyToMessageId).not.toBeDefined()
|
||||
expect(task.threadId).not.toBeDefined()
|
||||
expect(calledBody).toBe Message.attributes.body
|
||||
|
||||
describe "performRemote", ->
|
||||
|
|
|
@ -9,6 +9,11 @@ class ButtonDropdown extends React.Component
|
|||
primaryClick: React.PropTypes.func
|
||||
bordered: React.PropTypes.bool
|
||||
menu: React.PropTypes.element
|
||||
style: React.PropTypes.object
|
||||
closeOnMenuClick: React.PropTypes.bool
|
||||
|
||||
@defaultProps:
|
||||
style: {}
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = showing: false
|
||||
|
@ -19,7 +24,7 @@ class ButtonDropdown extends React.Component
|
|||
classnames += " bordered" if @props.bordered isnt false
|
||||
|
||||
if @props.primaryClick
|
||||
<div ref="button" onBlur={@_onBlur} tabIndex={999} className={classnames}>
|
||||
<div ref="button" onBlur={@_onBlur} tabIndex={999} className={classnames} style={@props.style}>
|
||||
<div className="primary-item"
|
||||
title={@props.primaryTitle ? ""}
|
||||
onClick={@props.primaryClick}>
|
||||
|
@ -28,19 +33,19 @@ class ButtonDropdown extends React.Component
|
|||
<div className="secondary-picker" onClick={@toggleDropdown}>
|
||||
<RetinaImg name={"icon-thread-disclosure.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</div>
|
||||
<div className="secondary-items">
|
||||
<div className="secondary-items" onMouseDown={@_onMenuClick}>
|
||||
{@props.menu}
|
||||
</div>
|
||||
</div>
|
||||
else
|
||||
<div ref="button" onBlur={@_onBlur} tabIndex={999} className={classnames}>
|
||||
<div ref="button" onBlur={@_onBlur} tabIndex={999} className={classnames} style={@props.style}>
|
||||
<div className="only-item"
|
||||
title={@props.primaryTitle ? ""}
|
||||
onClick={@toggleDropdown}>
|
||||
{@props.primaryItem}
|
||||
<RetinaImg name={"icon-thread-disclosure.png"} style={marginLeft:12} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</div>
|
||||
<div className="secondary-items left">
|
||||
<div className="secondary-items left" onMouseDown={@_onMenuClick}>
|
||||
{@props.menu}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -48,6 +53,10 @@ class ButtonDropdown extends React.Component
|
|||
toggleDropdown: =>
|
||||
@setState(showing: !@state.showing)
|
||||
|
||||
_onMenuClick: (event) =>
|
||||
if @props.closeOnMenuClick
|
||||
@setState showing: false
|
||||
|
||||
_onBlur: (event) =>
|
||||
target = event.nativeEvent.relatedTarget
|
||||
if target? and React.findDOMNode(@refs.button).contains(target)
|
||||
|
|
|
@ -80,6 +80,12 @@ module.exports =
|
|||
enum: ['reply', 'reply-all']
|
||||
enumLabels: ['Reply', 'Reply All']
|
||||
title: "Default reply behavior"
|
||||
defaultSendType:
|
||||
type: 'string'
|
||||
default: 'send'
|
||||
enum: ['send', 'send-and-archive']
|
||||
enumLabels: ['Send', 'Send and Archive']
|
||||
title: "Default send behavior"
|
||||
notifications:
|
||||
type: 'object'
|
||||
properties:
|
||||
|
|
|
@ -36,6 +36,34 @@ Section: Extensions
|
|||
###
|
||||
class ComposerExtension extends ContenteditableExtension
|
||||
|
||||
###
|
||||
Public: Allows the addition of new types of send actions such as "Send
|
||||
Later"
|
||||
|
||||
- `draft`: A fully populated {Message} object that is about to be sent.
|
||||
|
||||
Return an object that adheres to the following spec. If the draft data
|
||||
indicates that your action should not be available, then return null.
|
||||
|
||||
- `title`: A short, single string that is displayed to users when
|
||||
describing your component. It is used in the hover title text of your
|
||||
option in the dropdown menu. It is also used in the "Default Send
|
||||
Behavior" dropdown setting. If your string is selected, then the
|
||||
`core.sending.defaultSendType` will be set to your string and your
|
||||
option will appear as the default.
|
||||
## TODO FIXME: The preferences does not yet know how to dynamically
|
||||
# pick these up. For now they are hard-coded.
|
||||
|
||||
- `onSend`: Callback for when your option is clicked as the primary
|
||||
action. The function will be passed `{draft}` as its only argument.
|
||||
It does not need to return anything. It may be asynchronous and likely
|
||||
queue Tasks.
|
||||
|
||||
- `iconUrl`: A custom icon to be placed in the Send button. SendAction
|
||||
extensions have the form "Send + {ICON}"
|
||||
###
|
||||
@sendActionConfig: ({draft}) ->
|
||||
|
||||
###
|
||||
Public: Inspect the draft, and return any warnings that need to be
|
||||
displayed before the draft is sent. Warnings should be string phrases,
|
||||
|
|
|
@ -8,6 +8,7 @@ DraftStoreProxy = require './draft-store-proxy'
|
|||
DatabaseStore = require './database-store'
|
||||
AccountStore = require './account-store'
|
||||
ContactStore = require './contact-store'
|
||||
FocusedContentStore = require './focused-content-store'
|
||||
|
||||
SendDraftTask = require '../tasks/send-draft'
|
||||
DestroyDraftTask = require '../tasks/destroy-draft'
|
||||
|
@ -510,10 +511,15 @@ class DraftStore
|
|||
# committed to the Database since we'll look them up again just
|
||||
# before send.
|
||||
session.changes.commit(force: true, noSyncback: true).then =>
|
||||
draft = session.draft()
|
||||
# We unfortunately can't give the SendDraftTask the raw draft JSON
|
||||
# data because there may still be pending tasks (like a
|
||||
# {FileUploadTask}) that will continue to update the draft data.
|
||||
task = new SendDraftTask(draftClientId, {fromPopout: @_isPopout()})
|
||||
opts =
|
||||
threadId: draft.threadId
|
||||
replyToMessageId: draft.replyToMessageId
|
||||
|
||||
task = new SendDraftTask(draftClientId, opts)
|
||||
Actions.queueTask(task)
|
||||
|
||||
# NOTE: We may be done with the session in this window, but there
|
||||
|
@ -541,17 +547,23 @@ class DraftStore
|
|||
files = _.reject files, (f) -> f.id is file.id
|
||||
session.changes.add({files}, immediate: true)
|
||||
|
||||
_onDraftSendingFailed: ({draftClientId, errorMessage}) ->
|
||||
_onDraftSendingFailed: ({draftClientId, threadId, errorMessage}) ->
|
||||
@_draftsSending[draftClientId] = false
|
||||
@trigger(draftClientId)
|
||||
if NylasEnv.isMainWindow()
|
||||
# We delay so the view has time to update the restored draft. If we
|
||||
# don't delay the modal may come up in a state where the draft looks
|
||||
# like it hasn't been restored or has been lost.
|
||||
_.delay ->
|
||||
NylasEnv.showErrorDialog(errorMessage)
|
||||
_.delay =>
|
||||
@_notifyUserOfError({draftClientId, threadId, errorMessage})
|
||||
, 100
|
||||
|
||||
_notifyUserOfError: ({draftClientId, threadId, errorMessage}) ->
|
||||
focusedThread = FocusedContentStore.focused('thread')
|
||||
if threadId and focusedThread?.id is threadId
|
||||
NylasEnv.showErrorDialog(errorMessage)
|
||||
else
|
||||
Actions.composePopoutDraft(draftClientId, {errorMessage})
|
||||
|
||||
# Deprecations
|
||||
store = new DraftStore()
|
||||
|
|
|
@ -14,7 +14,7 @@ class NotFoundError extends Error
|
|||
module.exports =
|
||||
class SendDraftTask extends Task
|
||||
|
||||
constructor: (@draftClientId, {@fromPopout}={}) ->
|
||||
constructor: (@draftClientId, {@threadId, @replyToMessageId}={}) ->
|
||||
super
|
||||
|
||||
label: ->
|
||||
|
@ -161,7 +161,8 @@ class SendDraftTask extends Task
|
|||
return Promise.resolve([Task.Status.Failed, err])
|
||||
|
||||
_notifyUserOfError: (msg) =>
|
||||
if @fromPopout
|
||||
Actions.composePopoutDraft(@draftClientId, {errorMessage: msg})
|
||||
else
|
||||
Actions.draftSendingFailed({draftClientId: @draftClientId, errorMessage: msg})
|
||||
Actions.draftSendingFailed({
|
||||
threadId: @threadId
|
||||
draftClientId: @draftClientId,
|
||||
errorMessage: msg
|
||||
})
|
||||
|
|
|
@ -25,6 +25,44 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.dropdown-btn-emphasis {
|
||||
.primary-item,
|
||||
.secondary-picker,
|
||||
.only-item {
|
||||
.btn.btn-emphasis;
|
||||
}
|
||||
}
|
||||
|
||||
&.dropdown-btn-text {
|
||||
.primary-item,
|
||||
.secondary-picker,
|
||||
.only-item {
|
||||
.btn.btn-text;
|
||||
height: 28px;
|
||||
line-height: 1;
|
||||
}
|
||||
.primary-item, .only-item {
|
||||
padding-top: 5px;
|
||||
}
|
||||
.secondary-picker {
|
||||
padding: 4px 8px 2px 8px;
|
||||
}
|
||||
.secondary-items {
|
||||
.menu {
|
||||
.item {
|
||||
font-size: 13px;
|
||||
padding: 6px 11px;
|
||||
&:first-child {
|
||||
padding-top: 6px;
|
||||
}
|
||||
&:last-child {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.primary-item,
|
||||
.only-item {
|
||||
padding: 0.33em 1em;
|
||||
|
@ -130,5 +168,33 @@ body.platform-win32 {
|
|||
.secondary-items {
|
||||
border-radius: 0;
|
||||
}
|
||||
&.dropdown-btn-emphasis {
|
||||
.primary-item,
|
||||
.secondary-picker,
|
||||
.only-item {
|
||||
border: 0;
|
||||
background: @btn-emphasis-bg-color;
|
||||
&:hover {
|
||||
border-radius: 0;
|
||||
background: darken(@btn-emphasis-bg-color, 10%);
|
||||
}
|
||||
&:active {
|
||||
background: @btn-emphasis-bg-color;
|
||||
box-shadow: 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.dropdown-btn-text {
|
||||
.primary-item,
|
||||
.secondary-picker,
|
||||
.only-item {
|
||||
.btn.btn-text;
|
||||
padding-top: 5px;
|
||||
height: 28px;
|
||||
}
|
||||
.secondary-picker {
|
||||
padding: 4px 8px 2px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue