mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
feat(onboarding): refactor onboarding flow
Summary: Add spinner and refactor container view to be router add `NylasStore` as a global importable. specs for APIEnv login page fixes add old fixes to container view finish extracting pages fix onboarding flow Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D1652
This commit is contained in:
parent
4638c5bc5b
commit
fe1e18740c
21 changed files with 530 additions and 277 deletions
11
exports/nylas-store.coffee
Normal file
11
exports/nylas-store.coffee
Normal file
|
@ -0,0 +1,11 @@
|
|||
{Listener, Publisher} = require '../src/flux/modules/reflux-coffee'
|
||||
CoffeeHelpers = require '../src/flux/coffee-helpers'
|
||||
|
||||
# A simple Flux implementation
|
||||
class NylasStore
|
||||
@include: CoffeeHelpers.includeModule
|
||||
|
||||
@include Publisher
|
||||
@include Listener
|
||||
|
||||
module.exports = NylasStore
|
31
internal_packages/onboarding/lib/connect-account-page.cjsx
Normal file
31
internal_packages/onboarding/lib/connect-account-page.cjsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
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>
|
||||
<button className="btn btn-larger btn-gradient" onClick={=> @_fireAuthAccount('salesforce')}>Salesforce</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
_fireAuthAccount: (service) =>
|
||||
url = EdgehillAPI.urlForConnecting(service)
|
||||
OnboardingActions.moveToPage "add-account-auth", {url}
|
||||
|
||||
module.exports = ConnectAccountPage
|
|
@ -1,181 +0,0 @@
|
|||
React = require 'react/addons'
|
||||
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
OnboardingStore = require './onboarding-store'
|
||||
querystring = require 'querystring'
|
||||
{EdgehillAPI} = require 'nylas-exports'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class ContainerView extends React.Component
|
||||
@displayName: 'ContainerView'
|
||||
@containerRequired: false
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @getStateFromStore()
|
||||
|
||||
getStateFromStore: =>
|
||||
page: OnboardingStore.page()
|
||||
error: OnboardingStore.error()
|
||||
environment: OnboardingStore.environment()
|
||||
connectType: OnboardingStore.connectType()
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribe = OnboardingStore.listen(@_onStateChanged, @)
|
||||
|
||||
# It's important that every React class explicitly stops listening to
|
||||
# atom events before it unmounts. Thank you event-kit
|
||||
# This can be fixed via a Reflux mixin
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe() if @unsubscribe
|
||||
|
||||
componentDidUpdate: =>
|
||||
webview = @refs['connect-iframe']
|
||||
if webview
|
||||
node = React.findDOMNode(webview)
|
||||
if node.hasListeners is undefined
|
||||
# Remove as soon as possible. Initial src is not correctly loaded
|
||||
# on webview, and this fixes it. Electron 0.26.0. (Still in 0.28.1)
|
||||
setTimeout ->
|
||||
node.src = node.src
|
||||
,10
|
||||
node.addEventListener 'new-window', (e) ->
|
||||
require('shell').openExternal(e.url)
|
||||
node.addEventListener 'did-start-loading', (e) ->
|
||||
if node.hasMobileUserAgent is undefined
|
||||
node.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53")
|
||||
node.hasMobileUserAgent = true
|
||||
node.reload()
|
||||
node.addEventListener 'did-finish-load', (e) ->
|
||||
if node.getUrl().indexOf('/connect/complete') != -1
|
||||
query = node.getUrl().split('?')[1]
|
||||
query = query[0..-2] if query[query.length - 1] is '#'
|
||||
token = querystring.decode(query)
|
||||
OnboardingActions.finishedConnect(token)
|
||||
if node.getUrl().indexOf('cancelled') != -1
|
||||
OnboardingActions.moveToPreviousPage()
|
||||
|
||||
render: =>
|
||||
<div className="onboarding-container">
|
||||
<ReactCSSTransitionGroup transitionName="page">
|
||||
{@_pageComponent()}
|
||||
<div className="dragRegion" style={"WebkitAppRegion": "drag", position: 'absolute', top:0, left:40, right:0, height: 20, zIndex:100}></div>
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
|
||||
_pageComponent: =>
|
||||
if @state.error
|
||||
alert = <div className="alert alert-danger" role="alert">{@state.error}</div>
|
||||
else
|
||||
alert = <div></div>
|
||||
|
||||
if @state.page is 'welcome'
|
||||
<div className="page" key={@state.page}>
|
||||
<div className="quit" onClick={@_fireQuit}>
|
||||
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
<RetinaImg name="onboarding-logo.png" className="logo"/>
|
||||
<h2>Welcome to Nylas</h2>
|
||||
|
||||
<RetinaImg name="onboarding-divider.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
<form role="form" className="thin-container">
|
||||
<div className="prompt">Enter your email address:</div>
|
||||
<input className="input-bordered"
|
||||
type="email"
|
||||
placeholder="you@gmail.com"
|
||||
tabIndex="1"
|
||||
value={@state.email}
|
||||
onChange={@_onValueChange}
|
||||
id="email"
|
||||
spellCheck="false"/>
|
||||
<button className="btn btn-larger btn-gradient" style={width:215} onClick={@_fireStart}>Start using Nylas</button>
|
||||
{@_environmentComponent()}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
else if @state.page == 'add-account'
|
||||
<div className="page" key={@state.page}>
|
||||
<div className="quit" onClick={@_fireDismiss}>
|
||||
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
<RetinaImg name="onboarding-logo.png" className="logo" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
<h2>Connect an Account</h2>
|
||||
|
||||
<RetinaImg name="onboarding-divider.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
<form role="form" className="thin-container">
|
||||
<div className="prompt">Link accounts from other services to supercharge your email.</div>
|
||||
<button className="btn btn-larger btn-gradient" onClick={=> @_fireAuthAccount('salesforce')}>Salesforce</button>
|
||||
<button className="btn btn-larger btn-gradient" onClick={=> @_fireAuthAccount('linkedin')}>LinkedIn</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
else if @state.page == 'add-account-auth'
|
||||
<div>
|
||||
{
|
||||
React.createElement('webview',{
|
||||
"ref": "connect-iframe",
|
||||
"key": @state.page,
|
||||
"src": @_connectWebViewURL()
|
||||
})
|
||||
}
|
||||
<div className="back" onClick={@_fireMoveToPrevPage}>
|
||||
<RetinaImg name="onboarding-back.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
else if @state.page == 'add-account-success'
|
||||
# http://codepen.io/stevenfabre/pen/NPWeVb
|
||||
<div className="page" key={@state.page}>
|
||||
<div className="check">
|
||||
<svg preserveAspectRatio="xMidYMid" width="61" height="52" viewBox="0 0 61 52" className="check-icon">
|
||||
<path d="M56.560,-0.010 C37.498,10.892 26.831,26.198 20.617,33.101 C20.617,33.101 5.398,23.373 5.398,23.373 C5.398,23.373 0.010,29.051 0.010,29.051 C0.010,29.051 24.973,51.981 24.973,51.981 C29.501,41.166 42.502,21.583 60.003,6.565 C60.003,6.565 56.560,-0.010 56.560,-0.010 Z" id="path-1" className="cls-2" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_environmentComponent: =>
|
||||
return [] unless atom.inDevMode()
|
||||
<div className="environment-selector">
|
||||
<select value={@state.environment} onChange={@_fireSetEnvironment}>
|
||||
<option value="development">Development (edgehill-dev, api-staging)</option>
|
||||
<option value="staging">Staging (edgehill-staging, api-staging)</option>
|
||||
<option value="production">Production (edgehill, api)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
_connectWebViewURL: =>
|
||||
EdgehillAPI.urlForConnecting(@state.connectType, @state.email)
|
||||
|
||||
_onStateChanged: =>
|
||||
@setState(@getStateFromStore())
|
||||
|
||||
_onValueChange: (event) =>
|
||||
changes = {}
|
||||
changes[event.target.id] = event.target.value
|
||||
@setState(changes)
|
||||
|
||||
_fireDismiss: =>
|
||||
atom.close()
|
||||
|
||||
_fireQuit: =>
|
||||
require('ipc').send('command', 'application:quit')
|
||||
|
||||
_fireSetEnvironment: (event) =>
|
||||
OnboardingActions.setEnvironment(event.target.value)
|
||||
|
||||
_fireStart: (e) =>
|
||||
OnboardingActions.startConnect('inbox')
|
||||
|
||||
_fireAuthAccount: (service) =>
|
||||
OnboardingActions.startConnect(service)
|
||||
|
||||
_fireMoveToPage: (page) =>
|
||||
OnboardingActions.moveToPage(page)
|
||||
|
||||
_fireMoveToPrevPage: =>
|
||||
OnboardingActions.moveToPreviousPage()
|
||||
|
||||
|
||||
module.exports = ContainerView
|
|
@ -0,0 +1,95 @@
|
|||
React = require 'react'
|
||||
Page = require './page'
|
||||
querystring = require 'querystring'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{EdgehillAPI} = require 'nylas-exports'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
|
||||
class ExternalAuthWebviewPage extends Page
|
||||
@displayName: "ExternalAuthWebviewPage"
|
||||
|
||||
render: =>
|
||||
<div className="page no-top">
|
||||
{
|
||||
React.createElement('webview',{
|
||||
"ref": "connect-iframe",
|
||||
"src": @props.pageData.url
|
||||
"style": {position: "relative", zIndex: 1}
|
||||
})
|
||||
}
|
||||
{@_renderSpinner()}
|
||||
{@_renderAction()}
|
||||
</div>
|
||||
|
||||
componentDidMount: =>
|
||||
@_listeners = {}
|
||||
webview = @refs['connect-iframe']
|
||||
return unless webview
|
||||
webview = React.findDOMNode(webview)
|
||||
@_setupWebviewListeners(webview)
|
||||
|
||||
componentWillUnmount: ->
|
||||
webview = @refs['connect-iframe']
|
||||
webview = React.findDOMNode(webview)
|
||||
@_teardownWebviewListeners(webview)
|
||||
|
||||
_fireMoveToPrevPage: =>
|
||||
OnboardingActions.moveToPreviousPage()
|
||||
|
||||
_teardownWebviewListeners: (webview) ->
|
||||
for event, listener of @_listeners
|
||||
webview.removeEventListener event, listener
|
||||
|
||||
_renderAction: ->
|
||||
if @props.pageData.noPreviousPage
|
||||
@_renderClose()
|
||||
else
|
||||
<div className="back" onClick={@_fireMoveToPrevPage}>
|
||||
<RetinaImg name="onboarding-back.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}/>
|
||||
</div>
|
||||
|
||||
_setupWebviewListeners: (webview) ->
|
||||
# Remove as soon as possible. Initial src is not correctly loaded
|
||||
# on webview, and this fixes it. Electron 0.26.0. (Still in 0.28.1)
|
||||
setTimeout ->
|
||||
webview.src = webview.src
|
||||
, 20
|
||||
|
||||
@_listeners =
|
||||
"new-window": (e) ->
|
||||
require('shell').openExternal(e.url)
|
||||
"did-start-loading": (e) =>
|
||||
@_setUserAgent(e, webview)
|
||||
"did-finish-load": (e) =>
|
||||
@_onDidFinishLoad(e, webview)
|
||||
|
||||
for event, listener of @_listeners
|
||||
webview.addEventListener event, listener
|
||||
|
||||
_setUserAgent: (e, webview) ->
|
||||
if webview.hasMobileUserAgent is undefined
|
||||
webview.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53")
|
||||
webview.hasMobileUserAgent = true
|
||||
webview.reload()
|
||||
|
||||
_onDidFinishLoad: (e, webview) =>
|
||||
return unless webview
|
||||
|
||||
# We can't use `setState` because that'll blow away the webview :(
|
||||
React.findDOMNode(@refs.spinner).style.visibility = "hidden"
|
||||
|
||||
url = webview.getUrl()
|
||||
if url.indexOf('/connect/complete') != -1
|
||||
query = url.split('?')[1]
|
||||
query = query[0..-2] if query[query.length - 1] is '#'
|
||||
token = querystring.decode(query)
|
||||
|
||||
EdgehillAPI.addTokens([token])
|
||||
OnboardingActions.moveToPage('add-account-success')
|
||||
else if url.indexOf('cancelled') != -1
|
||||
OnboardingActions.moveToPreviousPage()
|
||||
|
||||
|
||||
|
||||
module.exports = ExternalAuthWebviewPage
|
87
internal_packages/onboarding/lib/login-page.cjsx
Normal file
87
internal_packages/onboarding/lib/login-page.cjsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
React = require 'react'
|
||||
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{EdgehillAPI} = require 'nylas-exports'
|
||||
|
||||
Page = require './page'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
NylasApiEnvironmentStore = require './nylas-api-environment-store'
|
||||
|
||||
class LoginPage extends Page
|
||||
@displayName: "LoginPage"
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
email: ""
|
||||
environment: NylasApiEnvironmentStore.getEnvironment()
|
||||
|
||||
componentDidMount: ->
|
||||
@_usub = NylasApiEnvironmentStore.listen =>
|
||||
@setState environment: NylasApiEnvironmentStore.getEnvironment()
|
||||
|
||||
componentWillUnmount: ->
|
||||
@_usub?()
|
||||
|
||||
render: =>
|
||||
<div className="page">
|
||||
{@_renderClose("quit")}
|
||||
|
||||
<RetinaImg name="onboarding-logo.png" mode={RetinaImg.Mode.ContentPreserve} className="logo"/>
|
||||
|
||||
<h2>Welcome to Nylas</h2>
|
||||
|
||||
<RetinaImg name="onboarding-divider.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
|
||||
<form role="form" ref="form" onSubmit={@_onSubmit} className="email-form thin-container">
|
||||
<div className="prompt">Enter your email address:</div>
|
||||
|
||||
<input type="email"
|
||||
required={true}
|
||||
ref="email"
|
||||
className="input-email input-bordered"
|
||||
placeholder="you@gmail.com"
|
||||
tabIndex="1"
|
||||
value={@state.email}
|
||||
onChange={@_onEmailChange}
|
||||
id="email"
|
||||
spellCheck="false"/>
|
||||
|
||||
<button className="btn btn-larger btn-gradient"
|
||||
style={width:215}>Start using Nylas</button>
|
||||
{@_environmentComponent()}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
_renderError: ->
|
||||
if @state.error
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{@state.error}
|
||||
</div>
|
||||
else <div></div>
|
||||
|
||||
_onEmailChange: (event) =>
|
||||
@setState email: event.target.value
|
||||
|
||||
_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">
|
||||
<select value={@state.environment} onChange={@_onEnvChange}>
|
||||
<option value="development">Development (edgehill-dev, api-staging)</option>
|
||||
<option value="staging">Staging (edgehill-staging, api-staging)</option>
|
||||
<option value="production">Production (edgehill, api)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
_onEnvChange: (event) =>
|
||||
OnboardingActions.changeAPIEnvironment event.target.value
|
||||
|
||||
module.exports = LoginPage
|
|
@ -1,11 +1,14 @@
|
|||
ContainerView = require './container-view'
|
||||
PageRouter = require "./page-router"
|
||||
{WorkspaceStore, ComponentRegistry} = require 'nylas-exports'
|
||||
|
||||
module.exports =
|
||||
item: null
|
||||
|
||||
activate: (@state) ->
|
||||
# This package does nothing in other windows
|
||||
return unless atom.getWindowType() is 'onboarding'
|
||||
|
||||
WorkspaceStore.defineSheet 'Main', {root: true},
|
||||
list: ['Center']
|
||||
ComponentRegistry.register ContainerView,
|
||||
ComponentRegistry.register PageRouter,
|
||||
location: WorkspaceStore.Location.Center
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
Actions = require './onboarding-actions'
|
||||
NylasStore = require 'nylas-store'
|
||||
|
||||
class NylasApiEnvironmentStore extends NylasStore
|
||||
constructor: ->
|
||||
@listenTo Actions.changeAPIEnvironment, @_setEnvironment
|
||||
|
||||
defaultEnv = if atom.inDevMode() then 'staging' else 'staging'
|
||||
@_setEnvironment(defaultEnv) unless atom.config.get('env')
|
||||
|
||||
getEnvironment: -> atom.config.get('env')
|
||||
|
||||
_setEnvironment: (env) ->
|
||||
throw new Error("Environment #{env} is not allowed") unless env in ['development', 'staging', 'production']
|
||||
atom.config.set('env', env)
|
||||
@trigger()
|
||||
|
||||
module.exports = new NylasApiEnvironmentStore()
|
|
@ -1,13 +1,14 @@
|
|||
Reflux = require 'reflux'
|
||||
|
||||
actions = [
|
||||
"setEnvironment",
|
||||
"authErrorOccurred",
|
||||
"startConnect",
|
||||
"finishedConnect",
|
||||
"moveToPreviousPage",
|
||||
OnboardingActions = Reflux.createActions [
|
||||
"changeAPIEnvironment"
|
||||
"loadExternalAuthPage"
|
||||
|
||||
"moveToPreviousPage"
|
||||
"moveToPage"
|
||||
]
|
||||
|
||||
module.exports =
|
||||
Actions = Reflux.createActions(actions)
|
||||
for key, action of OnboardingActions
|
||||
action.sync = true
|
||||
|
||||
module.exports = OnboardingActions
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
Reflux = require 'reflux'
|
||||
Actions = require './onboarding-actions'
|
||||
{EdgehillAPI} = require 'nylas-exports'
|
||||
ipc = require 'ipc'
|
||||
|
||||
return unless atom.getWindowType() is "onboarding"
|
||||
|
||||
module.exports =
|
||||
OnboardingStore = Reflux.createStore
|
||||
init: ->
|
||||
@_error = ''
|
||||
@_page = atom.getLoadSettings().page || 'welcome'
|
||||
|
||||
@_pageStack = [@_page]
|
||||
|
||||
# For the time being, always use staging
|
||||
defaultEnv = if atom.inDevMode() then 'staging' else 'staging'
|
||||
atom.config.set('env', defaultEnv) unless atom.config.get('env')
|
||||
|
||||
@listenTo Actions.setEnvironment, @_onSetEnvironment
|
||||
@listenTo Actions.moveToPreviousPage, @_onMoveToPreviousPage
|
||||
@listenTo Actions.moveToPage, @_onMoveToPage
|
||||
@listenTo Actions.startConnect, @_onStartConnect
|
||||
@listenTo Actions.finishedConnect, @_onFinishedConnect
|
||||
|
||||
page: ->
|
||||
@_page
|
||||
|
||||
error: ->
|
||||
@_error
|
||||
|
||||
environment: ->
|
||||
atom.config.get('env')
|
||||
|
||||
connectType: ->
|
||||
@_connectType
|
||||
|
||||
_onMoveToPreviousPage: ->
|
||||
current = @_pageStack.pop()
|
||||
prev = @_pageStack.pop()
|
||||
@_onMoveToPage(prev)
|
||||
|
||||
_onMoveToPage: (page) ->
|
||||
@_error = null
|
||||
@_pageStack.push(page)
|
||||
@_page = page
|
||||
@trigger()
|
||||
|
||||
_onStartConnect: (service) ->
|
||||
@_connectType = service
|
||||
@_onMoveToPage('add-account-auth')
|
||||
|
||||
_onFinishedConnect: (token) ->
|
||||
EdgehillAPI.addTokens([token])
|
||||
@_onMoveToPage('add-account-success')
|
||||
|
||||
setTimeout ->
|
||||
atom.close()
|
||||
, 2500
|
||||
|
||||
_onSetEnvironment: (env) ->
|
||||
throw new Error("Environment #{env} is not allowed") unless env in ['development', 'staging', 'production']
|
||||
atom.config.set('env', env)
|
||||
@trigger()
|
41
internal_packages/onboarding/lib/page-router-store.coffee
Normal file
41
internal_packages/onboarding/lib/page-router-store.coffee
Normal file
|
@ -0,0 +1,41 @@
|
|||
Reflux = require 'reflux'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
NylasStore = require 'nylas-store'
|
||||
ipc = require 'ipc'
|
||||
|
||||
return unless atom.getWindowType() is "onboarding"
|
||||
|
||||
class PageRouterStore extends NylasStore
|
||||
constructor: ->
|
||||
atom.onWindowPropsReceived @_onWindowPropsChagned
|
||||
|
||||
@_page = atom.getWindowProps().page ? ''
|
||||
@_pageData = atom.getWindowProps().pageData ? {}
|
||||
|
||||
@_pageStack = [{page: @_page, pageData: @_pageData}]
|
||||
|
||||
@listenTo OnboardingActions.moveToPreviousPage, @_onMoveToPreviousPage
|
||||
@listenTo OnboardingActions.moveToPage, @_onMoveToPage
|
||||
|
||||
_onWindowPropsChagned: ({page, pageData}={}) =>
|
||||
@_onMoveToPage(page, pageData)
|
||||
|
||||
page: -> @_page
|
||||
|
||||
pageData: -> @_pageData
|
||||
|
||||
connectType: ->
|
||||
@_connectType
|
||||
|
||||
_onMoveToPreviousPage: ->
|
||||
current = @_pageStack.pop()
|
||||
prev = @_pageStack.pop()
|
||||
@_onMoveToPage(prev.page, prev.pageData)
|
||||
|
||||
_onMoveToPage: (page, pageData={}) ->
|
||||
@_pageStack.push({page, pageData})
|
||||
@_page = page
|
||||
@_pageData = pageData
|
||||
@trigger()
|
||||
|
||||
module.exports = new PageRouterStore()
|
62
internal_packages/onboarding/lib/page-router.cjsx
Normal file
62
internal_packages/onboarding/lib/page-router.cjsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
React = require 'react/addons'
|
||||
OnboardingActions = require './onboarding-actions'
|
||||
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
|
||||
PageRouterStore = require './page-router-store'
|
||||
|
||||
LoginPage = require './login-page'
|
||||
ConnectAccountPage = require './connect-account-page'
|
||||
ExternalAuthWebviewPage = require './external-auth-webview-page'
|
||||
SuccessPage = require './success-page'
|
||||
|
||||
class PageRouter extends React.Component
|
||||
@displayName: 'PageRouter'
|
||||
@containerRequired: false
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStore()
|
||||
window.OnboardingActions = OnboardingActions
|
||||
|
||||
_getStateFromStore: =>
|
||||
page: PageRouterStore.page()
|
||||
pageData: PageRouterStore.pageData()
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribe = PageRouterStore.listen(@_onStateChanged, @)
|
||||
|
||||
_onStateChanged: => @setState(@_getStateFromStore())
|
||||
|
||||
componentWillUnmount: => @unsubscribe?()
|
||||
|
||||
render: =>
|
||||
<div className="page-frame">
|
||||
<ReactCSSTransitionGroup transitionName="page">
|
||||
{@_renderCurrentPage()}
|
||||
{@_renderDragRegion()}
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
|
||||
_renderCurrentPage: =>
|
||||
switch @state.page
|
||||
when "welcome"
|
||||
<LoginPage pageData={@state.pageData} />
|
||||
when "add-account"
|
||||
<ConnectAccountPage pageData={@state.pageData} />
|
||||
when "add-account-auth"
|
||||
<ExternalAuthWebviewPage pageData={@state.pageData} />
|
||||
when "add-account-success"
|
||||
<SuccessPage pageData={@state.pageData} />
|
||||
else
|
||||
<div></div>
|
||||
|
||||
_renderDragRegion: ->
|
||||
styles =
|
||||
top:0
|
||||
left:40
|
||||
right:0
|
||||
height: 20
|
||||
zIndex:100
|
||||
position: 'absolute'
|
||||
"WebkitAppRegion": "drag"
|
||||
<div className="dragRegion" style={styles}></div>
|
||||
|
||||
module.exports = PageRouter
|
36
internal_packages/onboarding/lib/page.cjsx
Normal file
36
internal_packages/onboarding/lib/page.cjsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
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%"
|
||||
width: "256px"
|
||||
marginLeft: "-128px"
|
||||
marginTop: "-128px"
|
||||
|
||||
<RetinaImg ref="spinner"
|
||||
style={styles}
|
||||
name="setup-spinner.gif"
|
||||
mode={RetinaImg.Mode.ContentPreserve}/>
|
||||
|
||||
module.exports = Page
|
23
internal_packages/onboarding/lib/success-page.cjsx
Normal file
23
internal_packages/onboarding/lib/success-page.cjsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
React = require 'react'
|
||||
Page = require './page'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class SuccessPage extends Page
|
||||
@displayName: "SuccessPage"
|
||||
|
||||
componentDidMount: ->
|
||||
setTimeout ->
|
||||
atom.close()
|
||||
, 2500
|
||||
|
||||
render: =>
|
||||
# http://codepen.io/stevenfabre/pen/NPWeVb
|
||||
<div className="page">
|
||||
<div className="check">
|
||||
<svg preserveAspectRatio="xMidYMid" width="61" height="52" viewBox="0 0 61 52" className="check-icon">
|
||||
<path d="M56.560,-0.010 C37.498,10.892 26.831,26.198 20.617,33.101 C20.617,33.101 5.398,23.373 5.398,23.373 C5.398,23.373 0.010,29.051 0.010,29.051 C0.010,29.051 24.973,51.981 24.973,51.981 C29.501,41.166 42.502,21.583 60.003,6.565 C60.003,6.565 56.560,-0.010 56.560,-0.010 Z" id="path-1" className="cls-2" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
module.exports = SuccessPage
|
|
@ -9,5 +9,8 @@
|
|||
"atom": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
},
|
||||
"windowTypes": {
|
||||
"onboarding": true
|
||||
}
|
||||
}
|
||||
|
|
45
internal_packages/onboarding/spec/login-page-spec.cjsx
Normal file
45
internal_packages/onboarding/spec/login-page-spec.cjsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
_ = require "underscore"
|
||||
React = require "react/addons"
|
||||
ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
LoginPage = require '../lib/login-page'
|
||||
OnboardingActions = require '../lib/onboarding-actions'
|
||||
|
||||
describe "LoginPage", ->
|
||||
|
||||
it "shows env picker in Dev Mode", ->
|
||||
spyOn(atom, "inDevMode").andReturn true
|
||||
@loginPage = ReactTestUtils.renderIntoDocument(<LoginPage />)
|
||||
picker = ReactTestUtils.findRenderedDOMComponentWithClass(@loginPage, "environment-selector")
|
||||
expect(picker).toBeDefined()
|
||||
|
||||
it "hides env picker in other modes", ->
|
||||
spyOn(atom, "inDevMode").andReturn false
|
||||
@loginPage = ReactTestUtils.renderIntoDocument(<LoginPage />)
|
||||
expect(-> ReactTestUtils.findRenderedDOMComponentWithClass(@loginPage, "environment-selector")).toThrow()
|
||||
|
||||
it 'can change the environment', ->
|
||||
spyOn(atom, "inDevMode").andReturn true
|
||||
spyOn(OnboardingActions, "changeAPIEnvironment")
|
||||
@loginPage = ReactTestUtils.renderIntoDocument(<LoginPage />)
|
||||
sel = ReactTestUtils.findRenderedDOMComponentWithTag(@loginPage, "select")
|
||||
ReactTestUtils.Simulate.change(sel, {target: {value: 'staging'}})
|
||||
expect(OnboardingActions.changeAPIEnvironment).toHaveBeenCalledWith("staging")
|
||||
|
||||
describe "logging in", ->
|
||||
beforeEach ->
|
||||
@connectURL = "foo"
|
||||
spyOn(OnboardingActions, "moveToPage")
|
||||
@loginPage = ReactTestUtils.renderIntoDocument(<LoginPage />)
|
||||
|
||||
hasEmail = (email) ->
|
||||
page = OnboardingActions.moveToPage.calls[0].args[0]
|
||||
data = OnboardingActions.moveToPage.calls[0].args[1]
|
||||
expect(page).toBe "add-account-auth"
|
||||
expect(data.url.length).toBeGreaterThan 0
|
||||
|
||||
it "submits information when the form submits", ->
|
||||
@loginPage.setState email: "test@nylas.com"
|
||||
form = ReactTestUtils.findRenderedDOMComponentWithClass(@loginPage, 'email-form')
|
||||
ReactTestUtils.Simulate.submit(form)
|
||||
hasEmail("test@nylas.com")
|
|
@ -0,0 +1,35 @@
|
|||
Actions = require '../lib/onboarding-actions'
|
||||
NylasApiEnvironmentStore = require '../lib/nylas-api-environment-store'
|
||||
storeConstructor = NylasApiEnvironmentStore.constructor
|
||||
|
||||
describe "NylasApiEnvironmentStore", ->
|
||||
beforeEach ->
|
||||
spyOn(atom.config, "set")
|
||||
|
||||
it "doesn't set if it alreayd exists", ->
|
||||
spyOn(atom.config, "get").andReturn "staging"
|
||||
store = new storeConstructor()
|
||||
expect(atom.config.set).not.toHaveBeenCalled()
|
||||
|
||||
it "initializes with the correct default in dev mode", ->
|
||||
spyOn(atom, "inDevMode").andReturn true
|
||||
spyOn(atom.config, "get").andReturn undefined
|
||||
store = new storeConstructor()
|
||||
expect(atom.config.set).toHaveBeenCalledWith("env", "staging")
|
||||
|
||||
it "initializes with the correct default in production", ->
|
||||
spyOn(atom, "inDevMode").andReturn false
|
||||
spyOn(atom.config, "get").andReturn undefined
|
||||
store = new storeConstructor()
|
||||
expect(atom.config.set).toHaveBeenCalledWith("env", "staging")
|
||||
|
||||
describe "when setting the environment", ->
|
||||
it "sets from the desired action", ->
|
||||
Actions.changeAPIEnvironment("production")
|
||||
expect(atom.config.set).toHaveBeenCalledWith("env", "production")
|
||||
|
||||
it "throws if the env is invalid", ->
|
||||
expect( -> Actions.changeAPIEnvironment("bad")).toThrow()
|
||||
|
||||
it "throws if the env is blank", ->
|
||||
expect( -> Actions.changeAPIEnvironment()).toThrow()
|
|
@ -19,7 +19,7 @@
|
|||
100% { opacity: 1; border-width: 0; }
|
||||
}
|
||||
|
||||
.onboarding-container {
|
||||
.page-frame {
|
||||
width:100%;
|
||||
height:100%;
|
||||
background-color: @gray-lighter;
|
||||
|
@ -82,6 +82,9 @@
|
|||
width:100%;
|
||||
height:100%;
|
||||
padding-top:15%;
|
||||
&.no-top {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.quit {
|
||||
|
|
|
@ -607,16 +607,15 @@ class Atom extends Model
|
|||
|
||||
@commands.add 'atom-workspace',
|
||||
'atom-workspace:add-account': =>
|
||||
options =
|
||||
@newWindow
|
||||
title: 'Add an Account'
|
||||
page: 'add-account'
|
||||
width: 340
|
||||
height: 550
|
||||
toolbar: false
|
||||
resizable: false
|
||||
windowType: 'onboarding'
|
||||
windowPackages: ['onboarding']
|
||||
ipc.send('new-window', options)
|
||||
windowProps:
|
||||
page: 'add-account'
|
||||
|
||||
# Make sure we can't be made so small that the interface looks like crap
|
||||
@getCurrentWindow().setMinimumSize(875, 500)
|
||||
|
|
|
@ -108,9 +108,9 @@ class WindowManager
|
|||
height: 550
|
||||
resizable: false
|
||||
windowType: 'onboarding'
|
||||
windowPackages: ['onboarding']
|
||||
windowProps:
|
||||
'uniqueId': 'onboarding'
|
||||
page: "welcome"
|
||||
uniqueId: 'onboarding'
|
||||
|
||||
# Makes a new window appear of a certain `windowType`.
|
||||
#
|
||||
|
@ -188,7 +188,7 @@ class WindowManager
|
|||
# - windowPackages - A list of additional packages to load into a
|
||||
# window in addition to those declared in various `package.json`s
|
||||
#
|
||||
registerHotWindow: ({windowType, replenishNum, windowPackages}={}) ->
|
||||
registerHotWindow: ({windowType, replenishNum, windowPackages, windowOptions}={}) ->
|
||||
if not windowType
|
||||
throw new Error("registerHotWindow: please provide a windowType")
|
||||
|
||||
|
@ -197,6 +197,7 @@ class WindowManager
|
|||
@_hotWindows[windowType].replenishNum ?= (replenishNum ? 1)
|
||||
@_hotWindows[windowType].loadedWindows ?= []
|
||||
@_hotWindows[windowType].windowPackages ?= (windowPackages ? [])
|
||||
@_hotWindows[windowType].windowOptions ?= (windowOptions ? {})
|
||||
|
||||
@_replenishHotWindows()
|
||||
|
||||
|
@ -248,6 +249,8 @@ class WindowManager
|
|||
newColdWindow: (options={}) ->
|
||||
options = _.extend(@defaultWindowOptions(), options)
|
||||
win = new AtomWindow(options)
|
||||
newLoadSettings = _.extend(win.loadSettings(), options)
|
||||
win.setLoadSettings(newLoadSettings)
|
||||
win.showWhenLoaded()
|
||||
return win
|
||||
|
||||
|
@ -262,9 +265,9 @@ class WindowManager
|
|||
win = null
|
||||
|
||||
if not hotWindowParams?
|
||||
console.log "WindowManager: Warning! The requested windowType '#{options.windowType}'
|
||||
has not been registered. Be sure to call `registerWindowType` first
|
||||
in your packages setup."
|
||||
console.log "WindowManager: Warning! The requested windowType
|
||||
'#{options.windowType}' has not been registered. Be sure to call
|
||||
`registerWindowType` first in your packages setup."
|
||||
return @newColdWindow(options)
|
||||
|
||||
supportedHotWindowKeys = [
|
||||
|
@ -279,22 +282,25 @@ class WindowManager
|
|||
]
|
||||
|
||||
unsupported = _.difference(Object.keys(options), supportedHotWindowKeys)
|
||||
|
||||
if unsupported.length > 0
|
||||
console.log "WindowManager: Nylas will open a new hot window of type #{options.windowType},
|
||||
but you are passing options that can't be applied to the preloaded window
|
||||
(#{JSON.stringify(unsupported)}). Please change the options or pass the
|
||||
`coldStart:true` option to use a new window instead of a hot window. If
|
||||
it's just data for the window, please put them in the `windowProps` param."
|
||||
console.log "WindowManager: For the winodw of type
|
||||
#{options.windowType}, you are passing options that can't be
|
||||
applied to the preloaded window (#{JSON.stringify(unsupported)}).
|
||||
Please change the options or pass the `coldStart:true` option to use
|
||||
a new window instead of a hot window. If it's just data for the
|
||||
window, please put them in the `windowProps` param."
|
||||
|
||||
if hotWindowParams.loadedWindows.length is 0
|
||||
# No windows ready
|
||||
console.log "No windows ready. Loading a new coldWindow"
|
||||
options.windowPackages = hotWindowParams.windowPackages
|
||||
win = @newColdWindow(options)
|
||||
else
|
||||
[win] = hotWindowParams.loadedWindows.splice(0,1)
|
||||
|
||||
newLoadSettings = _.extend(win.loadSettings(), options)
|
||||
win.setLoadSettings(newLoadSettings)
|
||||
win.showWhenLoaded()
|
||||
|
||||
win.browserWindow.setTitle options.title ? ""
|
||||
|
||||
|
@ -307,12 +313,11 @@ class WindowManager
|
|||
h = options.height ? h
|
||||
win.browserWindow.setSize(w,h)
|
||||
|
||||
console.log JSON.stringify(options)
|
||||
if options.bounds
|
||||
console.log "------------- SETTING BOUNDS"
|
||||
console.log JSON.stringify(options.bounds)
|
||||
win.browserWindow.setBounds options.bounds
|
||||
|
||||
win.showWhenLoaded()
|
||||
|
||||
@_replenishHotWindows()
|
||||
|
||||
return win
|
||||
|
@ -340,7 +345,7 @@ class WindowManager
|
|||
numOfType = data.replenishNum - data.loadedWindows.length
|
||||
maxWin = Math.max(numOfType, maxWin)
|
||||
if numOfType > 0
|
||||
options = @defaultWindowOptions()
|
||||
options = _.extend {}, @defaultWindowOptions(), data.windowOptions
|
||||
options.windowType = windowType
|
||||
options.windowPackages = data.windowPackages
|
||||
queues[windowType] ?= []
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
module.exports = CoffeeHelpers =
|
||||
# This copied out CoffeeScript
|
||||
includeModule: (mixin) ->
|
||||
if not mixin
|
||||
|
|
BIN
static/images/empty-state/Setup-Spinner.gif
Normal file
BIN
static/images/empty-state/Setup-Spinner.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
Loading…
Reference in a new issue