mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-20 23:36:21 +08:00
fix(onboarding): Lots of changes to account management, dead code cleanup
Summary: Better error handling in the account settings page and a loading spinner Add Account... replaces "Link External Account", and it works Clean dead code from onboarding pages, remove base class component Always show the account switcher rm dead EdgehillAPI code, AccountStore now manages accounts and credentials in config, not in database Fix specs Test Plan: Run tests Reviewers: dillon, evan Reviewed By: evan Projects: #edgehill Differential Revision: https://phab.nylas.com/D2059
This commit is contained in:
parent
f85b5d60ac
commit
297320df94
|
@ -20,8 +20,7 @@ class AccountSwitcher extends React.Component
|
|||
unsubscribe() for unsubscribe in @unsubscribers
|
||||
|
||||
render: =>
|
||||
return undefined if @state.accounts.length < 1
|
||||
|
||||
return false unless @state.account
|
||||
<div id="account-switcher" tabIndex={-1} onBlur={@_onBlur} ref="button">
|
||||
{@_renderAccount(@state.account, true)}
|
||||
{@_renderDropdown()}
|
||||
|
@ -125,8 +124,9 @@ class AccountSwitcher extends React.Component
|
|||
@setState(showing: false)
|
||||
|
||||
_onAddAccount: =>
|
||||
require('remote').getGlobal('application').windowManager.newOnboardingWindow()
|
||||
@setState showing: false
|
||||
ipc = require('ipc')
|
||||
ipc.send('command', 'application:add-account')
|
||||
@setState(showing: false)
|
||||
|
||||
_getStateFromStores: =>
|
||||
accounts: AccountStore.items()
|
||||
|
|
|
@ -79,10 +79,13 @@
|
|||
}
|
||||
|
||||
#account-switcher {
|
||||
padding-top: @padding-large-vertical;
|
||||
padding-bottom: @padding-base-vertical;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
|
||||
.primary-item {
|
||||
padding-top: @padding-large-vertical;
|
||||
padding-bottom: @padding-base-vertical;
|
||||
}
|
||||
|
||||
.account {
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -3,13 +3,12 @@ _ = require 'underscore'
|
|||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{EdgehillAPI, Utils} = require 'nylas-exports'
|
||||
|
||||
Page = require './page'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
NylasApiEnvironmentStore = require './nylas-api-environment-store'
|
||||
Providers = require './account-types'
|
||||
url = require 'url'
|
||||
|
||||
class AccountChoosePage extends Page
|
||||
class AccountChoosePage extends React.Component
|
||||
@displayName: "AccountChoosePage"
|
||||
|
||||
constructor: (@props) ->
|
||||
|
@ -27,7 +26,9 @@ class AccountChoosePage extends Page
|
|||
|
||||
render: =>
|
||||
<div className="page account-choose">
|
||||
{@_renderClose("quit")}
|
||||
<div className="quit" onClick={ => atom.close() }>
|
||||
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve}/>
|
||||
</div>
|
||||
|
||||
<div className="logo-container">
|
||||
<RetinaImg name="onboarding-logo.png" mode={RetinaImg.Mode.ContentPreserve} className="logo"/>
|
||||
|
@ -88,14 +89,6 @@ class AccountChoosePage extends Page
|
|||
})
|
||||
shell.openExternal(googleUrl)
|
||||
|
||||
_onSubmit: (e) =>
|
||||
valid = React.findDOMNode(@refs.form).reportValidity()
|
||||
if valid
|
||||
url = EdgehillAPI.urlForConnecting("inbox", @state.email)
|
||||
OnboardingActions.moveToPage("add-account-auth", {url})
|
||||
else
|
||||
e.preventDefault()
|
||||
|
||||
_environmentComponent: =>
|
||||
return <div></div> unless atom.inDevMode()
|
||||
<div className="environment-selector">
|
||||
|
|
|
@ -4,12 +4,11 @@ ipc = require 'ipc'
|
|||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{EdgehillAPI, NylasAPI, APIError} = require 'nylas-exports'
|
||||
|
||||
Page = require './page'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
NylasApiEnvironmentStore = require './nylas-api-environment-store'
|
||||
Providers = require './account-types'
|
||||
|
||||
class AccountSettingsPage extends Page
|
||||
class AccountSettingsPage extends React.Component
|
||||
@displayName: "AccountSettingsPage"
|
||||
|
||||
constructor: (@props) ->
|
||||
|
@ -18,6 +17,8 @@ class AccountSettingsPage extends Page
|
|||
settings: {}
|
||||
fields: {}
|
||||
pageNumber: 0
|
||||
errorFieldNames: []
|
||||
errorMessage: null
|
||||
show_advanced: false
|
||||
|
||||
@props.pageData.provider.settings.forEach (field) =>
|
||||
|
@ -36,7 +37,7 @@ class AccountSettingsPage extends Page
|
|||
@_pollForGmailAccount((account) ->
|
||||
if account?
|
||||
done = true
|
||||
OnboardingActions.nylasAccountReceived(account)
|
||||
OnboardingActions.accountJSONReceived(account)
|
||||
else if tries < 10 and id is poll_attempt_id
|
||||
setTimeout(_retry, delay)
|
||||
delay *= 1.5 # exponential backoff
|
||||
|
@ -106,8 +107,8 @@ class AccountSettingsPage extends Page
|
|||
</h2>
|
||||
|
||||
_renderErrorMessage: =>
|
||||
if @state.error
|
||||
<div className="errormsg">{@state.error.message ? ""}</div>
|
||||
if @state.errorMessage
|
||||
<div className="errormsg">{@state.errorMessage ? ""}</div>
|
||||
|
||||
_fieldOnCurrentPage: (field) =>
|
||||
!@state.provider.pages || field.page is @state.pageNumber
|
||||
|
@ -115,7 +116,7 @@ class AccountSettingsPage extends Page
|
|||
_renderFields: =>
|
||||
@state.provider.fields?.filter(@_fieldOnCurrentPage)
|
||||
.map (field, idx) =>
|
||||
errclass = if field.name in (@state.error?.invalid_fields ? []) then "error " else ""
|
||||
errclass = if field.name in @state.errorFieldNames then "error " else ""
|
||||
<label className={(field.className || "")} key={field.name}>
|
||||
{field.label}
|
||||
<input type={field.type}
|
||||
|
@ -141,7 +142,7 @@ class AccountSettingsPage extends Page
|
|||
{field.label}
|
||||
</label>
|
||||
else
|
||||
errclass = if field.name in (@state.error?.invalid_settings ? []) then "error " else ""
|
||||
errclass = if field.name in @state.errorFieldNames then "error " else ""
|
||||
<label className={field.className ? ""}
|
||||
style={if field.advanced and not @state.show_advanced then {display:'none'} else {}}
|
||||
key={field.name}>
|
||||
|
@ -157,16 +158,21 @@ class AccountSettingsPage extends Page
|
|||
|
||||
_renderButton: =>
|
||||
pages = @state.provider.pages || []
|
||||
if pages.length > @state.pageNumber+1
|
||||
if pages.length > @state.pageNumber + 1
|
||||
<button className="btn btn-large btn-gradient" type="button" onClick={@_onNextButton}>Next</button>
|
||||
else if @state.provider.name isnt 'gmail'
|
||||
<button className="btn btn-large btn-gradient" type="button" onClick={@_submit}>Set up account</button>
|
||||
if @state.tryingToAuthenticate
|
||||
<button className="btn btn-large btn-gradient btn-setup-spinning" type="button">
|
||||
<RetinaImg name="sending-spinner.gif" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} /> Setting up…
|
||||
</button>
|
||||
else
|
||||
<button className="btn btn-large btn-gradient" type="button" onClick={@_onSubmit}>Set up account</button>
|
||||
|
||||
_onNextButton: (event) =>
|
||||
@setState(pageNumber: @state.pageNumber+1)
|
||||
@setState(pageNumber: @state.pageNumber + 1)
|
||||
@_resize()
|
||||
|
||||
_submit: (event) =>
|
||||
_onSubmit: (event) =>
|
||||
data = settings: {}
|
||||
for own k,v of @state.fields when v isnt ''
|
||||
data[k] = v
|
||||
|
@ -178,6 +184,8 @@ class AccountSettingsPage extends Page
|
|||
if data.provider in ['exchange','outlook'] and not data.settings.username?.trim().length
|
||||
data.settings.username = data.email
|
||||
|
||||
@setState(tryingToAuthenticate: true)
|
||||
|
||||
# Send the form data directly to Nylas to get code
|
||||
# If this succeeds, send the received code to Edgehill server to register the account
|
||||
# Otherwise process the error message from the server and highlight UI as needed
|
||||
|
@ -186,6 +194,7 @@ class AccountSettingsPage extends Page
|
|||
method: 'POST'
|
||||
body: data
|
||||
returnsModel: false
|
||||
timeout: 30000
|
||||
auth:
|
||||
user: ''
|
||||
pass: ''
|
||||
|
@ -196,51 +205,50 @@ class AccountSettingsPage extends Page
|
|||
method: "POST"
|
||||
body: json
|
||||
success: (json) =>
|
||||
OnboardingActions.nylasAccountReceived(json)
|
||||
error: (err) =>
|
||||
throw err
|
||||
.catch APIError, (err) =>
|
||||
err_page_numbers = [@state.pageNumber]
|
||||
OnboardingActions.accountJSONReceived(json)
|
||||
error: @_onNetworkError
|
||||
.catch(@_onNetworkError)
|
||||
|
||||
if err.body.missing_fields?
|
||||
err.body.invalid_fields = err.body.missing_fields
|
||||
_onNetworkError: (err) =>
|
||||
errorMessage = err.message
|
||||
pageNumber = @state.pageNumber
|
||||
errorFieldNames = err.body?.missing_fields || err.body?.missing_settings
|
||||
|
||||
missing_fields = []
|
||||
for missing in err.body.missing_fields
|
||||
for f in @state.provider.fields when f.name is missing
|
||||
missing_fields.push(f.label.toLowerCase())
|
||||
if f.page isnt undefined
|
||||
err_page_numbers.push(f.page)
|
||||
if errorFieldNames
|
||||
{pageNumber, errorMessage} = @_stateForMissingFieldNames(errorFieldNames)
|
||||
if err.statusCode is -123 # timeout
|
||||
errorMessage = "Request timed out. Please try again."
|
||||
|
||||
err.body.message = @_missing_fields_message(missing_fields)
|
||||
@setState
|
||||
pageNumber: pageNumber
|
||||
errorMessage: errorMessage
|
||||
errorFieldNames: errorFieldNames || []
|
||||
tryingToAuthenticate: false
|
||||
@_resize()
|
||||
|
||||
else if err.body.missing_settings?
|
||||
err.body.invalid_settings = err.body.missing_settings
|
||||
_stateForMissingFieldNames: (fieldNames) ->
|
||||
fieldLabels = []
|
||||
fields = [].concat(@state.provider.settings, @state.provider.fields)
|
||||
pageNumbers = [@state.pageNumber]
|
||||
|
||||
missing_settings = []
|
||||
for missing in err.body.missing_settings
|
||||
for s in @state.provider.settings when s.name is missing
|
||||
missing_settings.push(s.label.toLowerCase())
|
||||
if s.page isnt undefined
|
||||
err_page_numbers.push(s.page)
|
||||
for fieldName in fieldNames
|
||||
for s in fields when s.name is fieldName
|
||||
fieldLabels.push(s.label.toLowerCase())
|
||||
if s.page isnt undefined
|
||||
pageNumbers.push(s.page)
|
||||
|
||||
err.body.message = @_missing_fields_message(missing_settings)
|
||||
pageNumber = Math.min.apply(null, pageNumbers)
|
||||
errorMessage = @_messageForFieldLabels(fieldLabels)
|
||||
|
||||
console.log(Math.min.apply(err_page_numbers), err_page_numbers)
|
||||
@setState(error: err.body, pageNumber: Math.min.apply(null,err_page_numbers))
|
||||
@_resize()
|
||||
console.log(err)
|
||||
{pageNumber, errorMessage}
|
||||
|
||||
_missing_fields_message: (missing_settings) ->
|
||||
if missing_settings.length > 2
|
||||
_messageForFieldLabels: (labels) ->
|
||||
if labels.length > 2
|
||||
return "Please fix the highlighted fields."
|
||||
else if missing_settings.length is 2
|
||||
first = missing_settings[0]
|
||||
last = missing_settings[1]
|
||||
return "Please provide your #{first} and #{last}."
|
||||
else if labels.length is 2
|
||||
return "Please provide your #{labels[0]} and #{labels[1]}."
|
||||
else
|
||||
setting = missing_settings[0]
|
||||
return "Please provide your #{setting}."
|
||||
return "Please provide your #{labels[0]}."
|
||||
|
||||
_pollForGmailAccount: (callback) =>
|
||||
EdgehillAPI.request
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
React = require 'react'
|
||||
Page = require './page'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{EdgehillAPI} = require 'nylas-exports'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
|
||||
class ConnectAccountPage extends Page
|
||||
@displayName: "ConnectAccountPage"
|
||||
|
||||
render: =>
|
||||
<div className="page">
|
||||
{@_renderClose("close")}
|
||||
|
||||
<RetinaImg name="onboarding-logo.png" mode={RetinaImg.Mode.ContentPreserve} className="logo"/>
|
||||
|
||||
<h2>Connect an Account</h2>
|
||||
|
||||
<RetinaImg name="onboarding-divider.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
<div className="thin-container">
|
||||
<div className="prompt">Link accounts from other services to supercharge your email.</div>
|
||||
<p>No more external accounts to link. Add additional features with plugins to Nylas Mail.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
_fireAuthAccount: (service) =>
|
||||
url = EdgehillAPI.urlForConnecting(service)
|
||||
OnboardingActions.moveToPage "add-account-auth", {url}
|
||||
|
||||
module.exports = ConnectAccountPage
|
|
@ -1,5 +1,4 @@
|
|||
React = require 'react'
|
||||
Page = require './page'
|
||||
{RetinaImg, ConfigPropContainer} = require 'nylas-component-kit'
|
||||
{EdgehillAPI} = require 'nylas-exports'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
|
@ -58,7 +57,7 @@ class InitialPackagesList extends React.Component
|
|||
else
|
||||
atom.packages.disablePackage(packageName)
|
||||
|
||||
class InitialPackagesPage extends Page
|
||||
class InitialPackagesPage extends React.Component
|
||||
@displayName: "InitialPackagesPage"
|
||||
|
||||
render: =>
|
||||
|
@ -84,6 +83,6 @@ class InitialPackagesPage extends Page
|
|||
|
||||
_onGetStarted: =>
|
||||
ipc = require 'ipc'
|
||||
ipc.send('login-successful')
|
||||
ipc.send('account-setup-successful')
|
||||
|
||||
module.exports = InitialPackagesPage
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
React = require 'react'
|
||||
Page = require './page'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
{RetinaImg, Flexbox, ConfigPropContainer} = require 'nylas-component-kit'
|
||||
|
@ -77,7 +76,7 @@ class InitialPreferencesOptions extends React.Component
|
|||
</div>
|
||||
|
||||
|
||||
class InitialPreferencesPage extends Page
|
||||
class InitialPreferencesPage extends React.Component
|
||||
@displayName: "InitialPreferencesPage"
|
||||
|
||||
render: =>
|
||||
|
|
|
@ -6,7 +6,7 @@ OnboardingActions = Reflux.createActions [
|
|||
|
||||
"moveToPreviousPage"
|
||||
"moveToPage"
|
||||
"nylasAccountReceived"
|
||||
"accountJSONReceived"
|
||||
]
|
||||
|
||||
for key, action of OnboardingActions
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
Reflux = require 'reflux'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
{AccountStore} = require 'nylas-exports'
|
||||
NylasStore = require 'nylas-store'
|
||||
ipc = require 'ipc'
|
||||
url = require 'url'
|
||||
|
@ -8,7 +9,7 @@ return unless atom.getWindowType() is "onboarding"
|
|||
|
||||
class PageRouterStore extends NylasStore
|
||||
constructor: ->
|
||||
atom.onWindowPropsReceived @_onWindowPropsChagned
|
||||
atom.onWindowPropsReceived @_onWindowPropsChanged
|
||||
|
||||
@_page = atom.getWindowProps().page ? ''
|
||||
@_pageData = atom.getWindowProps().pageData ? {}
|
||||
|
@ -17,20 +18,18 @@ class PageRouterStore extends NylasStore
|
|||
|
||||
@listenTo OnboardingActions.moveToPreviousPage, @_onMoveToPreviousPage
|
||||
@listenTo OnboardingActions.moveToPage, @_onMoveToPage
|
||||
@listenTo OnboardingActions.nylasAccountReceived, @_onNylasAccountReceived
|
||||
@listenTo OnboardingActions.accountJSONReceived, @_onAccountJSONReceived
|
||||
|
||||
_onNylasAccountReceived: (account) =>
|
||||
tokens = atom.config.get('tokens') || []
|
||||
tokens.push({
|
||||
provider: 'nylas'
|
||||
identifier: account.email_address
|
||||
access_token: account.auth_token
|
||||
})
|
||||
atom.config.set('tokens', tokens)
|
||||
atom.config.save()
|
||||
@_onMoveToPage('initial-preferences', {account})
|
||||
_onAccountJSONReceived: (json) =>
|
||||
isFirstAccount = AccountStore.items().length is 0
|
||||
AccountStore.addAccountFromJSON(json)
|
||||
atom.displayWindow()
|
||||
if isFirstAccount
|
||||
@_onMoveToPage('initial-preferences', {account: json})
|
||||
else
|
||||
ipc.send('account-setup-successful')
|
||||
|
||||
_onWindowPropsChagned: ({page, pageData}={}) =>
|
||||
_onWindowPropsChanged: ({page, pageData}={}) =>
|
||||
@_onMoveToPage(page, pageData)
|
||||
|
||||
page: -> @_page
|
||||
|
|
|
@ -23,21 +23,29 @@ class PageRouter extends React.Component
|
|||
|
||||
componentDidMount: =>
|
||||
@unsubscribe = PageRouterStore.listen(@_onStateChanged, @)
|
||||
setTimeout(@_initializeWindowSize, 10)
|
||||
|
||||
componentDidUpdate: =>
|
||||
setTimeout(@_updateWindowSize, 10)
|
||||
|
||||
_initializeWindowSize: =>
|
||||
return if @_unmounted
|
||||
{width, height} = React.findDOMNode(@refs.activePage).getBoundingClientRect()
|
||||
atom.center()
|
||||
atom.setSizeAnimated(width, height, 0)
|
||||
atom.show()
|
||||
|
||||
componentDidUpdate: =>
|
||||
setTimeout(@_resizePage, 10)
|
||||
|
||||
_resizePage: =>
|
||||
_updateWindowSize: =>
|
||||
return if @_unmounted
|
||||
{width, height} = React.findDOMNode(@refs.activePage).getBoundingClientRect()
|
||||
atom.setSizeAnimated(width, height)
|
||||
|
||||
_onStateChanged: => @setState(@_getStateFromStore())
|
||||
_onStateChanged: =>
|
||||
@setState(@_getStateFromStore())
|
||||
|
||||
componentWillUnmount: => @unsubscribe?()
|
||||
componentWillUnmount: =>
|
||||
@_unmounted = true
|
||||
@unsubscribe?()
|
||||
|
||||
render: =>
|
||||
<div className="page-frame">
|
||||
|
@ -72,7 +80,7 @@ class PageRouter extends React.Component
|
|||
}[@state.page]
|
||||
|
||||
<div key={@state.page} className="page-container">
|
||||
<Component pageData={@state.pageData} ref="activePage" onResize={@_resizePage}/>
|
||||
<Component pageData={@state.pageData} ref="activePage" onResize={@_updateWindowSize}/>
|
||||
</div>
|
||||
|
||||
_renderDragRegion: ->
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
React = require 'react'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class Page extends React.Component
|
||||
@displayName: "Page"
|
||||
|
||||
constructor: (@props) ->
|
||||
|
||||
_renderClose: (action="close") ->
|
||||
if action is "close"
|
||||
onClick = -> atom.close()
|
||||
else if action is "quit"
|
||||
onClick = ->
|
||||
require('ipc').send('command', 'application:quit')
|
||||
else onClick = ->
|
||||
|
||||
<div className="quit" onClick={onClick}>
|
||||
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve}/>
|
||||
</div>
|
||||
|
||||
_renderSpinner: ->
|
||||
styles =
|
||||
position: "absolute"
|
||||
zIndex: 10
|
||||
top: "50%"
|
||||
left: "50%"
|
||||
transform: 'translate(-50%, -50%)'
|
||||
|
||||
<RetinaImg ref="spinner"
|
||||
style={styles}
|
||||
name="Setup-Spinner.gif"
|
||||
mode={RetinaImg.Mode.ContentPreserve}/>
|
||||
|
||||
module.exports = Page
|
|
@ -1,9 +1,8 @@
|
|||
React = require 'react'
|
||||
Page = require './page'
|
||||
{RetinaImg, TimeoutTransitionGroup} = require 'nylas-component-kit'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
|
||||
class WelcomePage extends Page
|
||||
class WelcomePage extends React.Component
|
||||
@displayName: "WelcomePage"
|
||||
|
||||
constructor: (@props) ->
|
||||
|
@ -17,7 +16,10 @@ class WelcomePage extends Page
|
|||
buttons.push <button key="next" className="btn btn-large" onClick={@_onContinue}>Continue</button>
|
||||
|
||||
<div className="page no-top opaque" style={width: 667, display: "inline-block"}>
|
||||
{@_renderClose("close")}
|
||||
<div className="quit" onClick={ => atom.close() }>
|
||||
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve}/>
|
||||
</div>
|
||||
|
||||
<TimeoutTransitionGroup leaveTimeout={300}
|
||||
enterTimeout={300}
|
||||
className="welcome-image-container"
|
||||
|
|
|
@ -30,6 +30,10 @@
|
|||
font-size: 40pt;
|
||||
}
|
||||
|
||||
h2 {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
font-size: 20pt;
|
||||
|
@ -52,6 +56,9 @@
|
|||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.btn-setup-spinning {
|
||||
img { vertical-align: top; margin-right: 5px; }
|
||||
}
|
||||
.prompt {
|
||||
color:#5D5D5D;
|
||||
font-size:1.07em;
|
||||
|
@ -100,10 +107,11 @@
|
|||
}
|
||||
&.account-setup {
|
||||
padding: 0 15px;
|
||||
width: 300px
|
||||
width: 330px
|
||||
}
|
||||
&.account-choose {
|
||||
width: 300px;
|
||||
width: 330px;
|
||||
padding-bottom:10px;
|
||||
}
|
||||
&.opaque {
|
||||
background-color: @gray-lighter;
|
||||
|
@ -254,7 +262,8 @@
|
|||
}
|
||||
|
||||
.errormsg {
|
||||
color: #A33
|
||||
color: #A33;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
|
||||
form.settings {
|
||||
|
@ -297,6 +306,10 @@
|
|||
label.checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top:8px;
|
||||
}
|
||||
}
|
||||
|
||||
.appearance-mode {
|
||||
|
|
|
@ -6,9 +6,6 @@ _ = require 'underscore'
|
|||
class PreferencesAccounts extends React.Component
|
||||
@displayName: 'PreferencesAccounts'
|
||||
|
||||
@propTypes:
|
||||
config: React.PropTypes.object.isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @getStateFromStores()
|
||||
|
||||
|
@ -31,8 +28,6 @@ class PreferencesAccounts extends React.Component
|
|||
_renderAccounts: =>
|
||||
return false unless @state.accounts
|
||||
|
||||
allowUnlinking = @state.accounts.length > 1
|
||||
|
||||
<div>
|
||||
<div className="section-header">
|
||||
Accounts:
|
||||
|
@ -49,7 +44,7 @@ class PreferencesAccounts extends React.Component
|
|||
<div className="account-name">{account.emailAddress}</div>
|
||||
<div className="account-subtext">{account.name || "No name provided."} ({account.displayProvider()})</div>
|
||||
</div>
|
||||
<div style={textAlign:"right", marginTop:10, display: if allowUnlinking then 'inline-block' else 'none'}>
|
||||
<div style={textAlign:"right", marginTop:10, display:'inline-block'}>
|
||||
<button className="btn btn-large" onClick={ => @_onUnlinkAccount(account) }>Unlink</button>
|
||||
</div>
|
||||
</Flexbox>
|
||||
|
@ -94,29 +89,15 @@ class PreferencesAccounts extends React.Component
|
|||
tokens
|
||||
|
||||
_onAddAccount: =>
|
||||
require('remote').getGlobal('application').windowManager.newOnboardingWindow()
|
||||
ipc = require('ipc')
|
||||
ipc.send('command', 'application:add-account')
|
||||
|
||||
_onAccountChange: =>
|
||||
@setState(@getStateFromStores())
|
||||
|
||||
_onUnlinkAccount: (account) =>
|
||||
return [] unless @props.config
|
||||
|
||||
tokens = @props.config.get('tokens') || []
|
||||
token = _.find tokens, (t) ->
|
||||
t.provider is 'nylas' and t.identifier is account.emailAddress
|
||||
tokens = _.without(tokens, token)
|
||||
|
||||
if not token
|
||||
console.warn("Could not find nylas token for email address #{account.emailAddress}")
|
||||
return
|
||||
|
||||
DatabaseStore.unpersistModel(account).then =>
|
||||
# TODO: Delete other mail data
|
||||
EdgehillAPI.unlinkToken(token)
|
||||
AccountStore.removeAccountId(account.id)
|
||||
|
||||
_onUnlinkToken: (token) =>
|
||||
EdgehillAPI.unlinkToken(token)
|
||||
return
|
||||
|
||||
module.exports = PreferencesAccounts
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
{ type: 'separator' }
|
||||
{ label: 'Preferences', command: 'application:open-preferences' }
|
||||
{ type: 'separator' }
|
||||
{ label: 'Add Account...', command: 'atom-workspace:add-account' }
|
||||
{ label: 'Add Account...', command: 'application:add-account' }
|
||||
{ label: 'VERSION', enabled: false }
|
||||
{ label: 'Restart and Install Update', command: 'application:install-update', visible: false}
|
||||
{ label: 'Check for Update', command: 'application:check-for-update', visible: false}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{ label: '&New Message', command: 'application:new-message' }
|
||||
{ type: 'separator' }
|
||||
{ label: 'Preferences', command: 'application:open-preferences' }
|
||||
{ label: 'Add Account...', command: 'atom-workspace:add-account' }
|
||||
{ label: 'Add Account...', command: 'application:add-account' }
|
||||
{ label: 'Clos&e Window', command: 'window:close' }
|
||||
{ label: 'Quit', command: 'application:quit' }
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
'menu': [
|
||||
{ label: 'Add Account...', command: 'atom-workspace:add-account' }
|
||||
{ label: 'Add Account...', command: 'application:add-account' }
|
||||
{
|
||||
label: '&Edit'
|
||||
submenu: [
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
_ = require 'underscore'
|
||||
AccountStore = require '../../src/flux/stores/account-store'
|
||||
Account = require '../../src/flux/models/account'
|
||||
|
||||
describe "AccountStore", ->
|
||||
beforeEach ->
|
||||
|
@ -9,17 +10,38 @@ describe "AccountStore", ->
|
|||
afterEach ->
|
||||
@instance.stopListeningToAll()
|
||||
|
||||
it "should initialize current() using data saved in config", ->
|
||||
state =
|
||||
"id": "123",
|
||||
"email_address":"bengotow@gmail.com",
|
||||
"object":"account"
|
||||
"organization_unit": "label"
|
||||
it "should initialize using data saved in config", ->
|
||||
accounts =
|
||||
[{
|
||||
"id": "123",
|
||||
"client_id" : 'local-4f9d476a-c173',
|
||||
"server_id" : '123',
|
||||
"email_address":"bengotow@gmail.com",
|
||||
"object":"account"
|
||||
"organization_unit": "label"
|
||||
},{
|
||||
"id": "1234",
|
||||
"client_id" : 'local-4f9d476a-c175',
|
||||
"server_id" : '1234',
|
||||
"email_address":"ben@nylas.com",
|
||||
"object":"account"
|
||||
"organization_unit": "label"
|
||||
}]
|
||||
|
||||
spyOn(atom.config, 'get').andCallFake -> state
|
||||
spyOn(atom.config, 'get').andCallFake (key) ->
|
||||
if key is 'nylas.accounts'
|
||||
return accounts
|
||||
else if key is 'nylas.currentAccountIndex'
|
||||
return 1
|
||||
@instance = new @constructor
|
||||
expect(@instance.current().id).toEqual(state['id'])
|
||||
expect(@instance.current().emailAddress).toEqual(state['email_address'])
|
||||
|
||||
expect(@instance.items()).toEqual([
|
||||
(new Account).fromJSON(accounts[0]),
|
||||
(new Account).fromJSON(accounts[1])
|
||||
])
|
||||
expect(@instance.current() instanceof Account).toBe(true)
|
||||
expect(@instance.current().id).toEqual(accounts[1]['id'])
|
||||
expect(@instance.current().emailAddress).toEqual(accounts[1]['email_address'])
|
||||
|
||||
it "should initialize current() to null if data is not present", ->
|
||||
spyOn(atom.config, 'get').andCallFake -> null
|
||||
|
|
|
@ -631,18 +631,7 @@ class Atom extends Model
|
|||
CommandInstaller.installApmCommand resourcePath, false, (error) ->
|
||||
console.warn error.message if error?
|
||||
@commands.add 'atom-workspace',
|
||||
'atom-workspace:add-account': @onAddAccount
|
||||
|
||||
onAddAccount: =>
|
||||
@newWindow
|
||||
title: 'Add an Account'
|
||||
width: 340
|
||||
height: 550
|
||||
toolbar: false
|
||||
resizable: false
|
||||
windowType: 'onboarding'
|
||||
windowProps:
|
||||
page: 'add-account'
|
||||
'atom-workspace:add-account': @addAccount
|
||||
|
||||
# Call this method when establishing a secondary application window
|
||||
# displaying a specific set of packages.
|
||||
|
@ -777,6 +766,17 @@ class Atom extends Model
|
|||
executeJavaScriptInDevTools: (code) ->
|
||||
ipc.send('call-window-method', 'executeJavaScriptInDevTools', code)
|
||||
|
||||
addAccount: =>
|
||||
@newWindow
|
||||
title: 'Add an Account'
|
||||
width: 340
|
||||
height: 550
|
||||
toolbar: false
|
||||
resizable: false
|
||||
windowType: 'onboarding'
|
||||
windowProps:
|
||||
page: 'add-account'
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
|
|
@ -154,31 +154,31 @@ class Application
|
|||
app.commandLine.appendSwitch 'js-flags', '--harmony'
|
||||
|
||||
openWindowsForTokenState: (loadingMessage) =>
|
||||
hasToken = @config.get('tokens')?.length > 0
|
||||
if hasToken
|
||||
hasAccount = @config.get('nylas.accounts')?.length > 0
|
||||
if hasAccount
|
||||
@windowManager.showMainWindow(loadingMessage)
|
||||
@windowManager.ensureWorkWindow()
|
||||
else
|
||||
@windowManager.newOnboardingWindow()
|
||||
@windowManager.newOnboardingWindow({welcome: true})
|
||||
# The onboarding window automatically shows when it's ready
|
||||
|
||||
_resetConfigAndRelaunch: =>
|
||||
@setDatabasePhase('close')
|
||||
@windowManager.closeAllWindows()
|
||||
@_deleteDatabase =>
|
||||
@config.set('tokens', null)
|
||||
@config.set('nylas', null)
|
||||
@config.set('edgehill', null)
|
||||
@setDatabasePhase('setup')
|
||||
@openWindowsForTokenState()
|
||||
@windowManager.newOnboardingWindow({welcome: true})
|
||||
|
||||
_deleteDatabase: (callback) ->
|
||||
@deleteFileWithRetry path.join(configDirPath,'edgehill.db'), callback
|
||||
@deleteFileWithRetry path.join(configDirPath,'edgehill.db-wal')
|
||||
@deleteFileWithRetry path.join(configDirPath,'edgehill.db-shm')
|
||||
|
||||
_loginSuccessful: =>
|
||||
@openWindowsForTokenState()
|
||||
_accountSetupSuccessful: =>
|
||||
@windowManager.showMainWindow()
|
||||
@windowManager.ensureWorkWindow()
|
||||
@windowManager.mainWindow().waitForLoad =>
|
||||
@windowManager.onboardingWindow()?.close()
|
||||
|
||||
|
@ -247,6 +247,7 @@ class Application
|
|||
atomWindow ?= @windowManager.focusedWindow()
|
||||
atomWindow?.browserWindow.inspectElement(x, y)
|
||||
|
||||
@on 'application:add-account', => @windowManager.newOnboardingWindow()
|
||||
@on 'application:new-message', => @windowManager.sendToMainWindow('new-message')
|
||||
@on 'application:send-feedback', => @windowManager.sendToMainWindow('send-feedback')
|
||||
@on 'application:open-preferences', => @windowManager.sendToMainWindow('open-preferences')
|
||||
|
@ -257,6 +258,7 @@ class Application
|
|||
@quitting = true
|
||||
@windowManager.unregisterAllHotWindows()
|
||||
@autoUpdateManager.install()
|
||||
|
||||
@on 'application:open-dev', =>
|
||||
@devMode = true
|
||||
@windowManager.closeAllWindows()
|
||||
|
@ -375,8 +377,8 @@ class Application
|
|||
clipboard ?= require 'clipboard'
|
||||
clipboard.writeText(selectedText, 'selection')
|
||||
|
||||
ipc.on 'login-successful', (event) =>
|
||||
@_loginSuccessful()
|
||||
ipc.on 'account-setup-successful', (event) =>
|
||||
@_accountSetupSuccessful()
|
||||
|
||||
ipc.on 'run-in-window', (event, params) =>
|
||||
@_sourceWindows ?= {}
|
||||
|
|
|
@ -132,17 +132,23 @@ class WindowManager
|
|||
|
||||
# Returns a new onboarding window
|
||||
#
|
||||
newOnboardingWindow: ->
|
||||
@newWindow
|
||||
title: 'Welcome to Nylas'
|
||||
newOnboardingWindow: ({welcome} = {}) ->
|
||||
options =
|
||||
toolbar: false
|
||||
resizable: false
|
||||
hidden: true
|
||||
title: 'Add an Account'
|
||||
windowType: 'onboarding'
|
||||
windowProps:
|
||||
page: "welcome"
|
||||
page: 'account-choose'
|
||||
uniqueId: 'onboarding'
|
||||
|
||||
if welcome
|
||||
options.title = "Welcome to N1"
|
||||
options.windowProps.page = "welcome"
|
||||
|
||||
@newWindow(options)
|
||||
|
||||
# Makes a new window appear of a certain `windowType`.
|
||||
#
|
||||
# In almost all cases, instead of booting up a new window from scratch,
|
||||
|
|
|
@ -13,14 +13,6 @@ class EdgehillAPI
|
|||
constructor: ->
|
||||
atom.config.onDidChange('env', @_onConfigChanged)
|
||||
@_onConfigChanged()
|
||||
|
||||
# Always ask Edgehill Server for our tokens at launch. This way accounts
|
||||
# added elsewhere will appear, and we'll also handle the 0.2.5=>0.3.0 upgrade.
|
||||
if atom.isWorkWindow()
|
||||
existing = @_getCredentials()
|
||||
if existing and existing.username
|
||||
@setUserIdentifierAndRetrieveTokens(existing.username)
|
||||
|
||||
@
|
||||
|
||||
_onConfigChanged: =>
|
||||
|
@ -72,48 +64,6 @@ class EdgehillAPI
|
|||
else
|
||||
options.success(body) if options.success
|
||||
|
||||
urlForConnecting: (provider, email_address = '') ->
|
||||
auth = @_getCredentials()
|
||||
root = @APIRoot
|
||||
token = auth?.username
|
||||
"#{root}/connect/#{provider}?login_hint=#{email_address}&token=#{token}"
|
||||
|
||||
setUserIdentifierAndRetrieveTokens: (user_identifier) ->
|
||||
@_setCredentials(username: user_identifier, password: '')
|
||||
@request
|
||||
path: "/users/me"
|
||||
success: (userData={}) =>
|
||||
@setTokens(userData.tokens)
|
||||
if atom.getWindowType() is 'onboarding'
|
||||
ipc = require 'ipc'
|
||||
ipc.send('login-successful')
|
||||
error: (apiError) =>
|
||||
console.error apiError
|
||||
|
||||
setTokens: (incoming) ->
|
||||
# todo: remove once the edgehill-server inbox service is called `nylas`
|
||||
for token in incoming
|
||||
if token.provider is 'inbox'
|
||||
token.provider = 'nylas'
|
||||
atom.config.set('tokens', incoming)
|
||||
atom.config.save()
|
||||
|
||||
unlinkToken: (token) ->
|
||||
@request
|
||||
path: "/users/token/#{token.id}"
|
||||
method: 'DELETE'
|
||||
success: =>
|
||||
tokens = atom.config.get('tokens') || []
|
||||
tokens = _.reject tokens, (t) -> t.id is token.id
|
||||
atom.config.set('tokens', tokens)
|
||||
|
||||
accessTokenForProvider: (provider) ->
|
||||
tokens = atom.config.get('tokens') || []
|
||||
for token in tokens
|
||||
if token.provider is provider
|
||||
return token.access_token
|
||||
return null
|
||||
|
||||
_getCredentials: ->
|
||||
atom.config.get('edgehill.credentials')
|
||||
|
||||
|
|
|
@ -123,16 +123,11 @@ class NylasAPI
|
|||
@_optimisticChangeTracker = new NylasAPIOptimisticChangeTracker()
|
||||
|
||||
atom.config.onDidChange('env', @_onConfigChanged)
|
||||
atom.config.onDidChange('tokens', @_onConfigChanged)
|
||||
@_onConfigChanged()
|
||||
|
||||
_onConfigChanged: =>
|
||||
prev = {@AppID, @APIRoot, @APITokens}
|
||||
|
||||
tokens = atom.config.get('tokens') || []
|
||||
tokens = tokens.filter (t) -> t.provider is 'nylas'
|
||||
@APITokens = tokens.map (t) -> t.access_token
|
||||
|
||||
env = atom.config.get('env')
|
||||
if not env
|
||||
env = 'production'
|
||||
|
@ -154,13 +149,6 @@ class NylasAPI
|
|||
|
||||
current = {@AppID, @APIRoot, @APITokens}
|
||||
|
||||
if atom.isWorkWindow() and not _.isEqual(prev, current)
|
||||
@APITokens.forEach (token) =>
|
||||
@makeRequest
|
||||
path: "/account"
|
||||
auth: {'user': token, 'pass': '', sendImmediately: true}
|
||||
returnsModel: true
|
||||
|
||||
# Delegates to node's request object.
|
||||
# On success, it will call the passed in success callback with options.
|
||||
# On error it will create a new APIError object that wraps the error,
|
||||
|
@ -356,17 +344,8 @@ class NylasAPI
|
|||
decrementOptimisticChangeCount: (klass, id) ->
|
||||
@_optimisticChangeTracker.decrement(klass, id)
|
||||
|
||||
tokenObjectForAccountId: (aid) ->
|
||||
AccountStore = require './stores/account-store'
|
||||
accounts = AccountStore.items() || []
|
||||
account = _.find accounts, (acct) -> acct.id is aid
|
||||
return null unless account
|
||||
|
||||
tokens = atom.config.get('tokens') || []
|
||||
token = _.find tokens, (t) -> t.provider is 'nylas' and t.identifier is account.emailAddress
|
||||
return token
|
||||
|
||||
accessTokenForAccountId: (aid) ->
|
||||
@tokenObjectForAccountId(aid)?.access_token
|
||||
AccountStore = require './stores/account-store'
|
||||
AccountStore.tokenForAccountId(aid)
|
||||
|
||||
module.exports = new NylasAPI()
|
||||
|
|
|
@ -6,7 +6,9 @@ _ = require 'underscore'
|
|||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
||||
CoffeeHelpers = require '../coffee-helpers'
|
||||
|
||||
saveStateKey = "nylas.current_account"
|
||||
saveObjectsKey = "nylas.accounts"
|
||||
saveTokensKey = "nylas.accountTokens"
|
||||
saveIndexKey = "nylas.currentAccountIndex"
|
||||
|
||||
###
|
||||
Public: The AccountStore listens to changes to the available accounts in
|
||||
|
@ -21,49 +23,63 @@ class AccountStore
|
|||
@include Listener
|
||||
|
||||
constructor: ->
|
||||
@_items = []
|
||||
@_current = null
|
||||
@_accounts = []
|
||||
|
||||
saveState = atom.config.get(saveStateKey)
|
||||
if saveState and _.isObject(saveState)
|
||||
savedAccount = (new Account).fromJSON(saveState)
|
||||
if savedAccount.usesLabels() or savedAccount.usesFolders()
|
||||
@_setCurrent(savedAccount)
|
||||
@_accounts = [@_current]
|
||||
|
||||
@_load()
|
||||
@listenTo Actions.selectAccountId, @onSelectAccountId
|
||||
@listenTo DatabaseStore, @onDataChanged
|
||||
atom.config.observe saveTokensKey, (updatedTokens) =>
|
||||
return if _.isEqual(updatedTokens, @_tokens)
|
||||
newAccountIds = _.keys(_.omit(updatedTokens, _.keys(@_tokens)))
|
||||
if newAccountIds.length > 0
|
||||
Actions.selectAccountId(newAccountIds[0])
|
||||
@_load()
|
||||
|
||||
@populateItems()
|
||||
_load: =>
|
||||
@_accounts = []
|
||||
for json in atom.config.get(saveObjectsKey) || []
|
||||
@_accounts.push((new Account).fromJSON(json))
|
||||
|
||||
populateItems: =>
|
||||
DatabaseStore.findAll(Account).order(Account.attributes.emailAddress.descending()).then (accounts) =>
|
||||
current = _.find accounts, (a) -> a.id is @_current?.id
|
||||
current = accounts?[0] unless current
|
||||
index = atom.config.get(saveIndexKey) || 0
|
||||
@_index = Math.min(@_accounts.length - 1, Math.max(0, index))
|
||||
|
||||
if not _.isEqual(current, @_current) or not _.isEqual(accounts, @_accounts)
|
||||
@_setCurrent(current)
|
||||
@_accounts = accounts
|
||||
@trigger()
|
||||
@_tokens = atom.config.get(saveTokensKey) || {}
|
||||
@trigger()
|
||||
|
||||
.catch (err) =>
|
||||
console.warn("Request for accounts failed. #{err}", err.stack)
|
||||
|
||||
_setCurrent: (current) =>
|
||||
atom.config.set(saveStateKey, current)
|
||||
@_current = current
|
||||
_save: =>
|
||||
atom.config.set(saveObjectsKey, @_accounts)
|
||||
atom.config.set(saveIndexKey, @_index)
|
||||
atom.config.set(saveTokensKey, @_tokens)
|
||||
atom.config.save()
|
||||
|
||||
# Inbound Events
|
||||
|
||||
onDataChanged: (change) =>
|
||||
return unless change && change.objectClass is Account.name
|
||||
@populateItems()
|
||||
|
||||
onSelectAccountId: (id) =>
|
||||
return if @_current?.id is id
|
||||
@_current = _.find @_accounts, (a) -> a.id is id
|
||||
@trigger(@)
|
||||
idx = _.findIndex @_accounts, (a) -> a.id is id
|
||||
return if idx is -1
|
||||
atom.config.set(saveIndexKey, idx)
|
||||
@_index = idx
|
||||
@trigger()
|
||||
|
||||
removeAccountId: (id) =>
|
||||
idx = _.findIndex @_accounts, (a) -> a.id is id
|
||||
return if idx is -1
|
||||
|
||||
delete @_tokens[id]
|
||||
@_accounts.splice(idx, 1)
|
||||
@_save()
|
||||
|
||||
if @_accounts.length is 0
|
||||
ipc = require('ipc')
|
||||
ipc.send('command', 'application:reset-config-and-relaunch')
|
||||
else
|
||||
if @_index is idx
|
||||
Actions.selectAccountId(@_accounts[0].id)
|
||||
@trigger()
|
||||
|
||||
addAccountFromJSON: (json) =>
|
||||
return if @_tokens[json.id]
|
||||
@_tokens[json.id] = json.auth_token
|
||||
@_accounts.push((new Account).fromJSON(json))
|
||||
@_save()
|
||||
@trigger()
|
||||
|
||||
# Exposed Data
|
||||
|
||||
|
@ -73,6 +89,11 @@ class AccountStore
|
|||
|
||||
# Public: Returns the currently active {Account}.
|
||||
current: =>
|
||||
@_current
|
||||
@_accounts[@_index] || null
|
||||
|
||||
# Private: This method is going away soon, do not rely on it.
|
||||
#
|
||||
tokenForAccountId: (id) =>
|
||||
@_tokens[id]
|
||||
|
||||
module.exports = new AccountStore()
|
||||
|
|
BIN
static/images/onboarding/sending-spinner.gif
Normal file
BIN
static/images/onboarding/sending-spinner.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
Loading…
Reference in a new issue