feat(salesforce): load current user

Summary:
Loads the current user to pre-populate the Salesforce field in the
creators
add tabindex to tokenizing text fields

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1468
This commit is contained in:
Evan Morikawa 2015-05-07 15:28:44 -07:00
parent 753936b294
commit 2c60d75050
22 changed files with 185 additions and 73 deletions

View file

@ -24,6 +24,22 @@ class ComposerView extends React.Component
@containerRequired: false @containerRequired: false
@propTypes:
localId: React.PropTypes.string.isRequired
# 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
# Sometimes when changes in the composer happens it's desirable to
# have the parent scroll to a certain location. A parent component can
# pass a callback that gets called when this composer wants to be
# scrolled to.
onRequestScrollTo: React.PropTypes.func
constructor: (@props) -> constructor: (@props) ->
@state = @state =
populated: false populated: false
@ -211,7 +227,7 @@ class ComposerView extends React.Component
</div> </div>
<InjectedComponentSet <InjectedComponentSet
matching={role: "Composer:Footer"} matching={role: "Composer:Footer"}
exposedProps={draftLocalId:@props.localId}/> exposedProps={draftLocalId:@props.localId, threadId: @props.threadId}/>
</span> </span>
_renderActionsRegion: => _renderActionsRegion: =>
@ -219,7 +235,7 @@ class ComposerView extends React.Component
<InjectedComponentSet className="composer-action-bar-content" <InjectedComponentSet className="composer-action-bar-content"
matching={role: "Composer:ActionButton"} matching={role: "Composer:ActionButton"}
exposedProps={draftLocalId:@props.localId}> exposedProps={draftLocalId:@props.localId, threadId: @props.threadId}>
<button className="btn btn-toolbar btn-trash" style={order: 100} <button className="btn btn-toolbar btn-trash" style={order: 100}
data-tooltip="Delete draft" data-tooltip="Delete draft"

View file

@ -71,7 +71,7 @@ beforeEach ->
describe "A blank composer view", -> describe "A blank composer view", ->
beforeEach -> beforeEach ->
@composer = ReactTestUtils.renderIntoDocument( @composer = ReactTestUtils.renderIntoDocument(
<ComposerView /> <ComposerView localId="test123" />
) )
@composer.setState @composer.setState
body: "" body: ""

View file

@ -176,7 +176,7 @@ class MessageList extends React.Component
if message.draft if message.draft
components.push <InjectedComponent matching={role:"Composer"} components.push <InjectedComponent matching={role:"Composer"}
exposedProps={mode:"inline", localId:@state.messageLocalIds[message.id], onRequestScrollTo:@_onRequestScrollToComposer} exposedProps={mode:"inline", localId:@state.messageLocalIds[message.id], onRequestScrollTo:@_onRequestScrollToComposer, threadId:@state.currentThread.id}
ref="composerItem-#{message.id}" ref="composerItem-#{message.id}"
key={@state.messageLocalIds[message.id]} key={@state.messageLocalIds[message.id]}
className={className} /> className={className} />

View file

@ -408,18 +408,21 @@ describe "DraftStore", ->
expect(DraftStore.sendingState(draftLocalId)).toBe false expect(DraftStore.sendingState(draftLocalId)).toBe false
it "resets the sending state on success", -> it "resets the sending state on success", ->
DraftStore._onSendDraft(draftLocalId) waitsForPromise ->
expect(DraftStore.sendingState(draftLocalId)).toBe true DraftStore._onSendDraft(draftLocalId).then ->
DraftStore._onSendDraftSuccess(draftLocalId) expect(DraftStore.sendingState(draftLocalId)).toBe true
expect(DraftStore.sendingState(draftLocalId)).toBe false
expect(DraftStore.trigger).toHaveBeenCalled() DraftStore._onSendDraftSuccess({draftLocalId})
expect(DraftStore.sendingState(draftLocalId)).toBe false
expect(DraftStore.trigger).toHaveBeenCalled()
it "resets the sending state on error", -> it "resets the sending state on error", ->
DraftStore._onSendDraft(draftLocalId) waitsForPromise ->
expect(DraftStore.sendingState(draftLocalId)).toBe true DraftStore._onSendDraft(draftLocalId).then ->
DraftStore._onSendDraftError(draftLocalId) expect(DraftStore.sendingState(draftLocalId)).toBe true
expect(DraftStore.sendingState(draftLocalId)).toBe false DraftStore._onSendDraftError(draftLocalId)
expect(DraftStore.trigger).toHaveBeenCalled() expect(DraftStore.sendingState(draftLocalId)).toBe false
expect(DraftStore.trigger).toHaveBeenCalled()
it "closes the window if it's a popout", -> it "closes the window if it's a popout", ->
spyOn(atom, "getWindowType").andReturn "composer" spyOn(atom, "getWindowType").andReturn "composer"

View file

@ -111,8 +111,8 @@ describe "SendDraftTask", ->
email: 'dummy@inboxapp.com' email: 'dummy@inboxapp.com'
@draftLocalId = "local-123" @draftLocalId = "local-123"
@task = new SendDraftTask(@draftLocalId) @task = new SendDraftTask(@draftLocalId)
spyOn(atom.inbox, 'makeRequest').andCallFake (options) -> spyOn(atom.inbox, 'makeRequest').andCallFake (options) =>
options.success() if options.success options.success(@draft.toJSON()) if options.success
spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) => spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) =>
Promise.resolve(@draft) Promise.resolve(@draft)
spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) -> spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) ->
@ -127,7 +127,13 @@ describe "SendDraftTask", ->
it "should notify the draft was sent", -> it "should notify the draft was sent", ->
waitsForPromise => @task.performRemote().then => waitsForPromise => @task.performRemote().then =>
expect(Actions.sendDraftSuccess).toHaveBeenCalledWith(@draftLocalId) args = Actions.sendDraftSuccess.calls[0].args[0]
expect(args.draftLocalId).toBe @draftLocalId
it "get an object back on success", ->
waitsForPromise => @task.performRemote().then =>
args = Actions.sendDraftSuccess.calls[0].args[0]
expect(args.newMessage.id).toBe @draft.id
it "should play a sound", -> it "should play a sound", ->
waitsForPromise => @task.performRemote().then -> waitsForPromise => @task.performRemote().then ->

View file

@ -226,6 +226,8 @@ class Atom extends Model
@subscribe @packages.onDidActivateInitialPackages => @watchThemes() @subscribe @packages.onDidActivateInitialPackages => @watchThemes()
@windowEventHandler = new WindowEventHandler @windowEventHandler = new WindowEventHandler
window.onbeforeunload = => @onBeforeUnload()
# Start our error reporting to the backend and attach error handlers # Start our error reporting to the backend and attach error handlers
# to the window and the Bluebird Promise library, converting things # to the window and the Bluebird Promise library, converting things
# back through the sourcemap as necessary. # back through the sourcemap as necessary.
@ -452,6 +454,7 @@ class Atom extends Model
# currently loaded # currently loaded
loadSettingsChanged: (loadSettings) => loadSettingsChanged: (loadSettings) =>
@loadSettings = loadSettings @loadSettings = loadSettings
@constructor.loadSettings = loadSettings
{width, height, windowProps} = loadSettings {width, height, windowProps} = loadSettings
@packages.windowPropsReceived(windowProps ? {}) @packages.windowPropsReceived(windowProps ? {})
@ -872,3 +875,7 @@ class Atom extends Model
setAutoHideMenuBar: (autoHide) -> setAutoHideMenuBar: (autoHide) ->
ipc.send('call-window-method', 'setAutoHideMenuBar', autoHide) ipc.send('call-window-method', 'setAutoHideMenuBar', autoHide)
ipc.send('call-window-method', 'setMenuBarVisibility', !autoHide) ipc.send('call-window-method', 'setMenuBarVisibility', !autoHide)
onBeforeUnload: ->
Actions = require './flux/actions'
Actions.unloading()

View file

@ -96,8 +96,15 @@ class Popover extends React.Component
setTimeout => setTimeout =>
# Automatically focus the element inside us with the lowest tab index # Automatically focus the element inside us with the lowest tab index
node = React.findDOMNode(@refs.popover) node = React.findDOMNode(@refs.popover)
matches = _.sortBy node.querySelectorAll("[tabIndex]"), (a,b) -> a.tabIndex < b.tabIndex
matches[0].focus() if matches[0] # _.sortBy ranks in ascending numerical order.
matches = _.sortBy node.querySelectorAll("[tabIndex], input"), (node) ->
if node.tabIndex > 0
return node.tabIndex
else if node.nodeName is "INPUT"
return 1000000
else return 1000001
matches[0]?.focus()
_onBlur: (event) => _onBlur: (event) =>
target = event.nativeEvent.relatedTarget target = event.nativeEvent.relatedTarget

View file

@ -17,6 +17,12 @@ StylesImpactedByZoom = [
'marginRight' 'marginRight'
] ]
# We don't want to call `getLoadSettings` for each and every RetinaImg
# instance because it's a fairly expensive operation. Since the
# resourcePath can't change once the app has booted, it's safe to set the
# constant at require-time
DEFAULT_RESOURCE_PATH = atom.getLoadSettings().resourcePath
### ###
Public: RetinaImg wraps the DOM's standard `<img`> tag and implements a `UIImage` style Public: RetinaImg wraps the DOM's standard `<img`> tag and implements a `UIImage` style
interface. Rather than specifying an image `src`, RetinaImg allows you to provide interface. Rather than specifying an image `src`, RetinaImg allows you to provide
@ -38,6 +44,7 @@ class RetinaImg extends React.Component
- `colorfill` (optional) Adds -webkit-mask-image and other styles, and the .colorfill CSS - `colorfill` (optional) Adds -webkit-mask-image and other styles, and the .colorfill CSS
class, so that setting a CSS background color will colorfill the image. class, so that setting a CSS background color will colorfill the image.
- `style` (optional) An {Object} with additional styles to apply to the image. - `style` (optional) An {Object} with additional styles to apply to the image.
- `resourcePath` (options) Changes the default lookup location used to find the images.
### ###
@propTypes: @propTypes:
name: React.PropTypes.string name: React.PropTypes.string
@ -46,6 +53,7 @@ class RetinaImg extends React.Component
selected: React.PropTypes.bool selected: React.PropTypes.bool
active: React.PropTypes.bool active: React.PropTypes.bool
colorfill: React.PropTypes.bool colorfill: React.PropTypes.bool
resourcePath: React.PropTypes.string
render: -> render: ->
path = @_pathFor(@props.name) ? @_pathFor(@props.fallback) ? '' path = @_pathFor(@props.name) ? @_pathFor(@props.fallback) ? ''
@ -75,7 +83,8 @@ class RetinaImg extends React.Component
name = "#{basename}-active.#{ext}" name = "#{basename}-active.#{ext}"
if @props.selected is true if @props.selected is true
name = "#{basename}-selected.#{ext}" name = "#{basename}-selected.#{ext}"
Utils.imageNamed(name) resourcePath = @props.resourcePath ? DEFAULT_RESOURCE_PATH
Utils.imageNamed(resourcePath, name)
module.exports = RetinaImg module.exports = RetinaImg

View file

@ -70,6 +70,10 @@ TokenizingTextField = React.createClass
# change. # change.
tokens: React.PropTypes.arrayOf(React.PropTypes.object) tokens: React.PropTypes.arrayOf(React.PropTypes.object)
# The maximum number of tokens allowed. When null (the default) and
# unlimited number of tokens may be given
maxTokens: React.PropTypes.number
# A unique ID for each token object # A unique ID for each token object
# #
# A function that, given an object used for tokens, returns a unique # A function that, given an object used for tokens, returns a unique
@ -118,6 +122,10 @@ TokenizingTextField = React.createClass
# updates this component's `tokens` prop. # updates this component's `tokens` prop.
onAdd: React.PropTypes.func.isRequired onAdd: React.PropTypes.func.isRequired
# By default, when blurred, whatever is in the field is added. If this
# is true, the field will be cleared instead.
clearOnBlur: React.PropTypes.bool
# Gets called when we remove a token # Gets called when we remove a token
# #
# It's passed an array of objects (the same ones used to render # It's passed an array of objects (the same ones used to render
@ -186,7 +194,10 @@ TokenizingTextField = React.createClass
measure.innerText = @state.inputValue measure.innerText = @state.inputValue
measure.style.top = input.offsetTop + "px" measure.style.top = input.offsetTop + "px"
measure.style.left = input.offsetLeft + "px" measure.style.left = input.offsetLeft + "px"
input.style.width = "calc(6px + #{measure.offsetWidth}px)" if @_atMaxTokens()
input.style.width = "4px"
else
input.style.width = "calc(6px + #{measure.offsetWidth}px)"
render: -> render: ->
{Menu} = require 'ui-components' {Menu} = require 'ui-components'
@ -212,17 +223,7 @@ TokenizingTextField = React.createClass
<div className="tokenizing-field-input"> <div className="tokenizing-field-input">
{@_fieldTokenComponents()} {@_fieldTokenComponents()}
<input type="text" {@_inputEl()}
ref="input"
onCopy={@_onCopy}
onCut={@_onCut}
onPaste={@_onPaste}
onBlur={@_onInputBlurred}
onFocus={@_onInputFocused}
onChange={@_onInputChanged}
disabled={@props.disabled}
tabIndex={@props.tabIndex}
value={@state.inputValue} />
<span ref="measure" style={ <span ref="measure" style={
position: 'absolute' position: 'absolute'
visibility: 'hidden' visibility: 'hidden'
@ -230,6 +231,36 @@ TokenizingTextField = React.createClass
</div> </div>
</div> </div>
_inputEl: ->
if @_atMaxTokens()
<input type="text"
ref="input"
className="noop-input"
onCopy={@_onCopy}
onCut={@_onCut}
onBlur={@_onInputBlurred}
onFocus={ => @_onInputFocused(noCompletions: true)}
onChange={ -> "noop" }
tabIndex={@props.tabIndex}
value="" />
else
<input type="text"
ref="input"
onCopy={@_onCopy}
onCut={@_onCut}
onPaste={@_onPaste}
onBlur={@_onInputBlurred}
onFocus={@_onInputFocused}
onChange={@_onInputChanged}
disabled={@props.disabled}
tabIndex={@props.tabIndex}
value={@state.inputValue} />
_atMaxTokens: ->
if @props.maxTokens
@props.tokens.length >= @props.maxTokens
else return false
_renderPrompt: -> _renderPrompt: ->
if @props.menuPrompt if @props.menuPrompt
<div className="tokenizing-field-label">{"#{@props.menuPrompt}:"}</div> <div className="tokenizing-field-label">{"#{@props.menuPrompt}:"}</div>
@ -248,9 +279,9 @@ TokenizingTextField = React.createClass
# Maintaining Input State # Maintaining Input State
_onInputFocused: -> _onInputFocused: ({noCompletions}={}) ->
@setState focus: true @setState focus: true
@_refreshCompletions() @_refreshCompletions() unless noCompletions
_onInputChanged: (event) -> _onInputChanged: (event) ->
val = event.target.value.trimLeft() val = event.target.value.trimLeft()
@ -260,7 +291,11 @@ TokenizingTextField = React.createClass
@_refreshCompletions(val) @_refreshCompletions(val)
_onInputBlurred: -> _onInputBlurred: ->
@_addInputValue() if @props.clearOnBlur
@_clearInput()
else
@_addInputValue()
@_refreshCompletions("", clear: true)
@setState @setState
selectedTokenKey: null selectedTokenKey: null
focus: false focus: false
@ -275,6 +310,7 @@ TokenizingTextField = React.createClass
# Managing Tokens # Managing Tokens
_addInputValue: (input) -> _addInputValue: (input) ->
return if @_atMaxTokens()
input ?= @state.inputValue input ?= @state.inputValue
@props.onAdd(input) @props.onAdd(input)
@_clearInput() @_clearInput()

View file

@ -55,16 +55,25 @@ class UnsafeComponent extends React.Component
element = <component key={name} {...props} /> element = <component key={name} {...props} />
@injected = React.render(element, node) @injected = React.render(element, node)
catch err catch err
stack = err.stack if atom.inDevMode()
stackEnd = stack.indexOf('react/lib/') console.error err
if stackEnd > 0 stack = err.stack
stackEnd = stack.lastIndexOf('\n', stackEnd) console.log stack
stack = stack.substr(0,stackEnd) stackEnd = stack.indexOf('react/lib/')
if stackEnd > 0
stackEnd = stack.lastIndexOf('\n', stackEnd)
stack = stack.substr(0,stackEnd)
element = <div className="unsafe-component-exception"> element = <div className="unsafe-component-exception">
<div className="message">{@props.component.displayName} could not be displayed.</div> <div className="message">{@props.component.displayName} could not be displayed.</div>
<div className="trace">{stack}</div> <div className="trace">{stack}</div>
</div> </div>
else
## TODO
# Add some sort of notification code here that lets us know when
# production builds are having issues!
#
element = <div></div>
@injected = React.render(element, node) @injected = React.render(element, node)

View file

@ -125,6 +125,8 @@ windowActions = [
"metadataError", "metadataError",
"metadataCreated", "metadataCreated",
"metadataDestroyed" "metadataDestroyed"
"unloading" # Tied to `window.onbeforeunload`
] ]
allActions = [].concat(windowActions).concat(globalActions).concat(mainWindowActions) allActions = [].concat(windowActions).concat(globalActions).concat(mainWindowActions)

View file

@ -124,7 +124,6 @@ class Message extends Model
@naturalSortOrder: -> @naturalSortOrder: ->
Message.attributes.date.ascending() Message.attributes.date.ascending()
constructor: -> constructor: ->
super super
@subject ||= "" @subject ||= ""

View file

@ -135,22 +135,21 @@ Utils =
tableNameForJoin: (primaryKlass, secondaryKlass) -> tableNameForJoin: (primaryKlass, secondaryKlass) ->
"#{primaryKlass.name}-#{secondaryKlass.name}" "#{primaryKlass.name}-#{secondaryKlass.name}"
imageNamed: (fullname) -> imageNamed: (resourcePath, fullname) ->
[name, ext] = fullname.split('.') [name, ext] = fullname.split('.')
if Utils.images is undefined Utils.images ?= {}
start = Date.now() if not Utils.images[resourcePath]?
{resourcePath} = atom.getLoadSettings()
imagesPath = path.join(resourcePath, 'static', 'images') imagesPath = path.join(resourcePath, 'static', 'images')
files = fs.listTreeSync(imagesPath) files = fs.listTreeSync(imagesPath)
Utils.images = {} Utils.images[resourcePath] ?= {}
Utils.images[path.basename(file)] = file for file in files Utils.images[resourcePath][path.basename(file)] = file for file in files
if window.devicePixelRatio > 1 if window.devicePixelRatio > 1
return Utils.images["#{name}@2x.#{ext}"] ? Utils.images[fullname] ? Utils.images["#{name}@1x.#{ext}"] return Utils.images[resourcePath]["#{name}@2x.#{ext}"] ? Utils.images[resourcePath][fullname] ? Utils.images[resourcePath]["#{name}@1x.#{ext}"]
else else
return Utils.images["#{name}@1x.#{ext}"] ? Utils.images[fullname] ? Utils.images["#{name}@2x.#{ext}"] return Utils.images[resourcePath]["#{name}@1x.#{ext}"] ? Utils.images[resourcePath][fullname] ? Utils.images[resourcePath]["#{name}@2x.#{ext}"]
subjectWithPrefix: (subject, prefix) -> subjectWithPrefix: (subject, prefix) ->
if subject.search(/fwd:/i) is 0 if subject.search(/fwd:/i) is 0

View file

@ -30,7 +30,7 @@ AnalyticsStore = Reflux.createStore
fileAborted: (uploadData={}) -> {fileSize: uploadData.fileSize} fileAborted: (uploadData={}) -> {fileSize: uploadData.fileSize}
fileUploaded: (uploadData={}) -> {fileSize: uploadData.fileSize} fileUploaded: (uploadData={}) -> {fileSize: uploadData.fileSize}
sendDraftError: (dId, msg) -> {drafLocalId: dId, error: msg} sendDraftError: (dId, msg) -> {drafLocalId: dId, error: msg}
sendDraftSuccess: (draftLocalId) -> {draftLocalId: draftLocalId} sendDraftSuccess: ({draftLocalId}) -> {draftLocalId: draftLocalId}
showDeveloperConsole: -> {} showDeveloperConsole: -> {}
composeReply: ({threadId, messageId}) -> {threadId, messageId} composeReply: ({threadId, messageId}) -> {threadId, messageId}
composeForward: ({threadId, messageId}) -> {threadId, messageId} composeForward: ({threadId, messageId}) -> {threadId, messageId}

View file

@ -56,6 +56,7 @@ class DraftStore
@listenTo Actions.sendDraftError, @_onSendDraftError @listenTo Actions.sendDraftError, @_onSendDraftError
@listenTo Actions.sendDraftSuccess, @_onSendDraftSuccess @listenTo Actions.sendDraftSuccess, @_onSendDraftSuccess
@listenTo Actions.unloading, @_onBeforeUnload
@_draftSessions = {} @_draftSessions = {}
@_sendingState = {} @_sendingState = {}
@ -65,7 +66,6 @@ class DraftStore
# TODO: Doesn't work if we do window.addEventListener, but this is # TODO: Doesn't work if we do window.addEventListener, but this is
# fragile. Pending an Atom fix perhaps? # fragile. Pending an Atom fix perhaps?
window.onbeforeunload = => @_onBeforeUnload()
######### PUBLIC ####################################################### ######### PUBLIC #######################################################
@ -389,7 +389,7 @@ class DraftStore
@_onPopoutDraftLocalId(draftLocalId, {errorMessage}) @_onPopoutDraftLocalId(draftLocalId, {errorMessage})
@trigger() @trigger()
_onSendDraftSuccess: (draftLocalId) => _onSendDraftSuccess: ({draftLocalId}) =>
@_sendingState[draftLocalId] = false @_sendingState[draftLocalId] = false
@trigger() @trigger()

View file

@ -14,7 +14,7 @@ CreateMetadataTask = require '../tasks/create-metadata-task'
DestroyMetadataTask = require '../tasks/destroy-metadata-task' DestroyMetadataTask = require '../tasks/destroy-metadata-task'
# TODO: This Store is like many other stores (like the # TODO: This Store is like many other stores (like the
# SalesforceObjectStore or the SalesforceAssociationStore) in that it has # SalesforceObjectStore or the SalesforceThreadAssociationStore) in that it has
# to double cache data from the API and the DB with minor variation. # to double cache data from the API and the DB with minor variation.
# There's a task to refactor these stores into something like an # There's a task to refactor these stores into something like an
# `APIBackedStore` to abstract some of the complex logic out. # `APIBackedStore` to abstract some of the complex logic out.
@ -24,7 +24,6 @@ MAX_API_RATE = 1000
module.exports = module.exports =
MetadataStore = Reflux.createStore MetadataStore = Reflux.createStore
init: -> init: ->
return unless atom.isMainWindow()
@listenTo DatabaseStore, @_onDBChanged @listenTo DatabaseStore, @_onDBChanged
@listenTo NamespaceStore, @_onNamespaceChanged @listenTo NamespaceStore, @_onNamespaceChanged
@ -45,9 +44,8 @@ MetadataStore = Reflux.createStore
@_namespaceId = NamespaceStore.current()?.id @_namespaceId = NamespaceStore.current()?.id
@_metadata = {} @_metadata = {}
return if atom.inSpecMode()
@_fullRefreshFromAPI() @_fullRefreshFromAPI()
@_refreshCacheFromDB = _.debounce(_.bind(@_refreshCacheFromDB, @), 16) @_refreshCacheFromDB = _.debounce(_.bind(@_refreshCacheFromDB, @), 16)
@_refreshCacheFromDB() @_refreshCacheFromDB()
@ -62,10 +60,12 @@ MetadataStore = Reflux.createStore
else return null else return null
_fullRefreshFromAPI: -> _fullRefreshFromAPI: ->
return if not atom.isMainWindow() or atom.inSpecMode()
return unless @_namespaceId return unless @_namespaceId
@_apiRequest() # The lack of type will request everything! @_apiRequest() # The lack of type will request everything!
_refreshDBFromAPI: -> _refreshDBFromAPI: ->
return if not atom.isMainWindow() or atom.inSpecMode()
types = Object.keys(@_typesToRefresh) types = Object.keys(@_typesToRefresh)
@_typesToRefresh = {} @_typesToRefresh = {}
promises = types.map (type) => @_apiRequest(type) promises = types.map (type) => @_apiRequest(type)

View file

@ -48,10 +48,13 @@ class SendDraftTask extends Task
method: 'POST' method: 'POST'
body: body body: body
returnsModel: true returnsModel: true
success: => success: (newMessage) =>
newMessage = (new Message).fromJSON(newMessage)
atom.playSound('mail_sent.ogg') atom.playSound('mail_sent.ogg')
Actions.postNotification({message: "Sent!", type: 'success'}) Actions.postNotification({message: "Sent!", type: 'success'})
Actions.sendDraftSuccess(@draftLocalId) Actions.sendDraftSuccess
draftLocalId: @draftLocalId
newMessage: newMessage
DatabaseStore.unpersistModel(draft).then(resolve) DatabaseStore.unpersistModel(draft).then(resolve)
error: reject error: reject
.catch(reject) .catch(reject)

View file

@ -162,7 +162,7 @@ class Package
@activateConfig() @activateConfig()
@activateStylesheets() @activateStylesheets()
if @requireMainModule() if @requireMainModule()
@mainModule.activate(atom.packages.getPackageState(@name) ? {}) @mainModule.activate(atom.packages.getPackageState(@name) ? {}, path.resolve(@path))
@mainActivated = true @mainActivated = true
@activateServices() @activateServices()
catch e catch e
@ -385,6 +385,7 @@ class Package
return return
mainModulePath = @getMainModulePath() mainModulePath = @getMainModulePath()
@mainModule = require(mainModulePath) if fs.isFileSync(mainModulePath) @mainModule = require(mainModulePath) if fs.isFileSync(mainModulePath)
return @mainModule
getMainModulePath: -> getMainModulePath: ->
return @mainModulePath if @resolvedMainModulePath return @mainModulePath if @resolvedMainModulePath

View file

@ -181,7 +181,7 @@ class SheetContainer extends React.Component
sheetElements = @_sheetElements() sheetElements = @_sheetElements()
<Flexbox direction="column"> <Flexbox direction="column">
<div name="Toolbar" style={order:0} className="sheet-toolbar"> <div name="Toolbar" style={order:0, zIndex: 2} className="sheet-toolbar">
{toolbarElements[0]} {toolbarElements[0]}
<TimeoutTransitionGroup leaveTimeout={125} <TimeoutTransitionGroup leaveTimeout={125}
enterTimeout={125} enterTimeout={125}
@ -190,12 +190,12 @@ class SheetContainer extends React.Component
</TimeoutTransitionGroup> </TimeoutTransitionGroup>
</div> </div>
<div name="Header" style={order:1}> <div name="Header" style={order:1, zIndex: 3}>
<InjectedComponentSet matching={locations: [topSheet.Header, WorkspaceStore.Sheet.Global.Header]} <InjectedComponentSet matching={locations: [topSheet.Header, WorkspaceStore.Sheet.Global.Header]}
id={topSheet.id}/> id={topSheet.id}/>
</div> </div>
<div name="Center" style={order:2, flex: 1, position:'relative'}> <div name="Center" style={order:2, flex: 1, position:'relative', zIndex: 1}>
{sheetElements[0]} {sheetElements[0]}
<TimeoutTransitionGroup leaveTimeout={125} <TimeoutTransitionGroup leaveTimeout={125}
enterTimeout={125} enterTimeout={125}
@ -204,7 +204,7 @@ class SheetContainer extends React.Component
</TimeoutTransitionGroup> </TimeoutTransitionGroup>
</div> </div>
<div name="Footer" style={order:3}> <div name="Footer" style={order:3, zIndex: 4}>
<InjectedComponentSet matching={locations: [topSheet.Footer, WorkspaceStore.Sheet.Global.Footer]} <InjectedComponentSet matching={locations: [topSheet.Footer, WorkspaceStore.Sheet.Global.Footer]}
id={topSheet.id}/> id={topSheet.id}/>
</div> </div>

View file

@ -27,6 +27,10 @@
.input-area { .input-area {
position: relative; position: relative;
flex: 2; flex: 2;
select {
margin-top: 9px;
}
} }
.form-error { .form-error {

View file

@ -78,6 +78,10 @@
position: relative; position: relative;
padding-left: 2.1em; padding-left: 2.1em;
&:hover {
cursor: text;
}
input { input {
display: inline-block; display: inline-block;
width: initial; width: initial;
@ -87,7 +91,14 @@
min-width: 5em; min-width: 5em;
background-color:transparent; background-color:transparent;
vertical-align:bottom; vertical-align:bottom;
&.noop-input {
position: absolute;
min-width: 0;
padding-left: 0;
margin-right: 0;
}
} }
input:focus { input:focus {
box-shadow: none; box-shadow: none;
} }