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

@ -229,4 +229,4 @@ class Menu extends React.Component
Menu.Item = MenuItem Menu.Item = MenuItem
Menu.NameEmailItem = MenuNameEmailItem Menu.NameEmailItem = MenuNameEmailItem
module.exports = Menu module.exports = Menu

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
@ -106,4 +113,4 @@ class Popover extends React.Component
@setState @setState
showing:false showing:false
module.exports = Popover module.exports = Popover

View file

@ -17,11 +17,17 @@ 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
an image name. Like UIImage on iOS, it automatically finds the best image for the current an image name. Like UIImage on iOS, it automatically finds the best image for the current
display based on pixel density. Given `image.png`, on a Retina screen, it looks for display based on pixel density. Given `image.png`, on a Retina screen, it looks for
`image@2x.png`, `image.png`, `image@1x.png` in that order. It uses a lookup table and caches `image@2x.png`, `image.png`, `image@1x.png` in that order. It uses a lookup table and caches
image names, so images generally resolve immediately. image names, so images generally resolve immediately.
### ###
@ -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

@ -32,8 +32,8 @@ Token = React.createClass
"dragging": @getDragState('token').isDragging "dragging": @getDragState('token').isDragging
"selected": @props.selected "selected": @props.selected
<div {...@dragSourceFor('token')} <div {...@dragSourceFor('token')}
className={classes} className={classes}
onClick={@_onSelect}> onClick={@_onSelect}>
<button className="action" onClick={@_onAction} style={marginTop: "2px"}><RetinaImg name="composer-caret.png" /></button> <button className="action" onClick={@_onAction} style={marginTop: "2px"}><RetinaImg name="composer-caret.png" /></button>
{@props.children} {@props.children}
@ -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;
} }