mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-11-09 16:01:42 +08:00
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:
parent
753936b294
commit
2c60d75050
22 changed files with 185 additions and 73 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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: ""
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,6 @@ class Message extends Model
|
||||||
@naturalSortOrder: ->
|
@naturalSortOrder: ->
|
||||||
Message.attributes.date.ascending()
|
Message.attributes.date.ascending()
|
||||||
|
|
||||||
|
|
||||||
constructor: ->
|
constructor: ->
|
||||||
super
|
super
|
||||||
@subject ||= ""
|
@subject ||= ""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@
|
||||||
.input-area {
|
.input-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 2;
|
flex: 2;
|
||||||
|
|
||||||
|
select {
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-error {
|
.form-error {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue