From fe1e18740ccb21e4bfa30a388731f1f802c641d9 Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Wed, 17 Jun 2015 15:58:58 -0700 Subject: [PATCH] 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 --- exports/nylas-store.coffee | 11 ++ .../onboarding/lib/connect-account-page.cjsx | 31 +++ .../onboarding/lib/container-view.cjsx | 181 ------------------ .../lib/external-auth-webview-page.cjsx | 95 +++++++++ .../onboarding/lib/login-page.cjsx | 87 +++++++++ internal_packages/onboarding/lib/main.cjsx | 7 +- .../lib/nylas-api-environment-store.coffee | 18 ++ .../onboarding/lib/onboarding-actions.coffee | 17 +- .../onboarding/lib/onboarding-store.coffee | 64 ------- .../onboarding/lib/page-router-store.coffee | 41 ++++ .../onboarding/lib/page-router.cjsx | 62 ++++++ internal_packages/onboarding/lib/page.cjsx | 36 ++++ .../onboarding/lib/success-page.cjsx | 23 +++ internal_packages/onboarding/package.json | 3 + .../onboarding/spec/login-page-spec.cjsx | 45 +++++ .../nylas-api-environment-store-spec.coffee | 35 ++++ .../onboarding/stylesheets/onboarding.less | 5 +- src/atom.coffee | 7 +- src/browser/window-manager.coffee | 37 ++-- src/flux/coffee-helpers.coffee | 2 +- static/images/empty-state/Setup-Spinner.gif | Bin 0 -> 33997 bytes 21 files changed, 530 insertions(+), 277 deletions(-) create mode 100644 exports/nylas-store.coffee create mode 100644 internal_packages/onboarding/lib/connect-account-page.cjsx delete mode 100644 internal_packages/onboarding/lib/container-view.cjsx create mode 100644 internal_packages/onboarding/lib/external-auth-webview-page.cjsx create mode 100644 internal_packages/onboarding/lib/login-page.cjsx create mode 100644 internal_packages/onboarding/lib/nylas-api-environment-store.coffee delete mode 100644 internal_packages/onboarding/lib/onboarding-store.coffee create mode 100644 internal_packages/onboarding/lib/page-router-store.coffee create mode 100644 internal_packages/onboarding/lib/page-router.cjsx create mode 100644 internal_packages/onboarding/lib/page.cjsx create mode 100644 internal_packages/onboarding/lib/success-page.cjsx create mode 100644 internal_packages/onboarding/spec/login-page-spec.cjsx create mode 100644 internal_packages/onboarding/spec/nylas-api-environment-store-spec.coffee create mode 100644 static/images/empty-state/Setup-Spinner.gif diff --git a/exports/nylas-store.coffee b/exports/nylas-store.coffee new file mode 100644 index 000000000..59769ccac --- /dev/null +++ b/exports/nylas-store.coffee @@ -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 diff --git a/internal_packages/onboarding/lib/connect-account-page.cjsx b/internal_packages/onboarding/lib/connect-account-page.cjsx new file mode 100644 index 000000000..9fa8dcc77 --- /dev/null +++ b/internal_packages/onboarding/lib/connect-account-page.cjsx @@ -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: => +
+ {@_renderClose("close")} + + + +

Connect an Account

+ + + +
+
Link accounts from other services to supercharge your email.
+ +
+ +
+ + _fireAuthAccount: (service) => + url = EdgehillAPI.urlForConnecting(service) + OnboardingActions.moveToPage "add-account-auth", {url} + +module.exports = ConnectAccountPage diff --git a/internal_packages/onboarding/lib/container-view.cjsx b/internal_packages/onboarding/lib/container-view.cjsx deleted file mode 100644 index 198734a34..000000000 --- a/internal_packages/onboarding/lib/container-view.cjsx +++ /dev/null @@ -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: => -
- - {@_pageComponent()} -
-
-
- - _pageComponent: => - if @state.error - alert =
{@state.error}
- else - alert =
- - if @state.page is 'welcome' -
-
- -
- -

Welcome to Nylas

- - - -
-
Enter your email address:
- - - {@_environmentComponent()} -
- -
- - else if @state.page == 'add-account' -
-
- -
- -

Connect an Account

- - - -
-
Link accounts from other services to supercharge your email.
- - -
-
- - else if @state.page == 'add-account-auth' -
- { - React.createElement('webview',{ - "ref": "connect-iframe", - "key": @state.page, - "src": @_connectWebViewURL() - }) - } -
- -
-
- - else if @state.page == 'add-account-success' - # http://codepen.io/stevenfabre/pen/NPWeVb -
-
- - - -
-
- - _environmentComponent: => - return [] unless atom.inDevMode() -
- -
- - _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 diff --git a/internal_packages/onboarding/lib/external-auth-webview-page.cjsx b/internal_packages/onboarding/lib/external-auth-webview-page.cjsx new file mode 100644 index 000000000..368a8b99e --- /dev/null +++ b/internal_packages/onboarding/lib/external-auth-webview-page.cjsx @@ -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: => +
+ { + React.createElement('webview',{ + "ref": "connect-iframe", + "src": @props.pageData.url + "style": {position: "relative", zIndex: 1} + }) + } + {@_renderSpinner()} + {@_renderAction()} +
+ + 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 +
+ +
+ + _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 diff --git a/internal_packages/onboarding/lib/login-page.cjsx b/internal_packages/onboarding/lib/login-page.cjsx new file mode 100644 index 000000000..d120c4950 --- /dev/null +++ b/internal_packages/onboarding/lib/login-page.cjsx @@ -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: => +
+ {@_renderClose("quit")} + + + +

Welcome to Nylas

+ + + +
+
Enter your email address:
+ + + + + {@_environmentComponent()} +
+ +
+ + _renderError: -> + if @state.error +
+ {@state.error} +
+ else
+ + _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
unless atom.inDevMode() +
+ +
+ + _onEnvChange: (event) => + OnboardingActions.changeAPIEnvironment event.target.value + +module.exports = LoginPage diff --git a/internal_packages/onboarding/lib/main.cjsx b/internal_packages/onboarding/lib/main.cjsx index 89119beca..2f3c9bbe1 100644 --- a/internal_packages/onboarding/lib/main.cjsx +++ b/internal_packages/onboarding/lib/main.cjsx @@ -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 diff --git a/internal_packages/onboarding/lib/nylas-api-environment-store.coffee b/internal_packages/onboarding/lib/nylas-api-environment-store.coffee new file mode 100644 index 000000000..18efc33bb --- /dev/null +++ b/internal_packages/onboarding/lib/nylas-api-environment-store.coffee @@ -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() diff --git a/internal_packages/onboarding/lib/onboarding-actions.coffee b/internal_packages/onboarding/lib/onboarding-actions.coffee index 51e35b01a..0075f44ef 100644 --- a/internal_packages/onboarding/lib/onboarding-actions.coffee +++ b/internal_packages/onboarding/lib/onboarding-actions.coffee @@ -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 diff --git a/internal_packages/onboarding/lib/onboarding-store.coffee b/internal_packages/onboarding/lib/onboarding-store.coffee deleted file mode 100644 index 7473cf348..000000000 --- a/internal_packages/onboarding/lib/onboarding-store.coffee +++ /dev/null @@ -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() diff --git a/internal_packages/onboarding/lib/page-router-store.coffee b/internal_packages/onboarding/lib/page-router-store.coffee new file mode 100644 index 000000000..77eff3e63 --- /dev/null +++ b/internal_packages/onboarding/lib/page-router-store.coffee @@ -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() diff --git a/internal_packages/onboarding/lib/page-router.cjsx b/internal_packages/onboarding/lib/page-router.cjsx new file mode 100644 index 000000000..78136abc7 --- /dev/null +++ b/internal_packages/onboarding/lib/page-router.cjsx @@ -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: => +
+ + {@_renderCurrentPage()} + {@_renderDragRegion()} + +
+ + _renderCurrentPage: => + switch @state.page + when "welcome" + + when "add-account" + + when "add-account-auth" + + when "add-account-success" + + else +
+ + _renderDragRegion: -> + styles = + top:0 + left:40 + right:0 + height: 20 + zIndex:100 + position: 'absolute' + "WebkitAppRegion": "drag" +
+ +module.exports = PageRouter diff --git a/internal_packages/onboarding/lib/page.cjsx b/internal_packages/onboarding/lib/page.cjsx new file mode 100644 index 000000000..e69054f07 --- /dev/null +++ b/internal_packages/onboarding/lib/page.cjsx @@ -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 = -> + +
+ +
+ + _renderSpinner: -> + styles = + position: "absolute" + zIndex: 10 + top: "50%" + left: "50%" + width: "256px" + marginLeft: "-128px" + marginTop: "-128px" + + + +module.exports = Page diff --git a/internal_packages/onboarding/lib/success-page.cjsx b/internal_packages/onboarding/lib/success-page.cjsx new file mode 100644 index 000000000..c5b88fda4 --- /dev/null +++ b/internal_packages/onboarding/lib/success-page.cjsx @@ -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 +
+
+ + + +
+
+ +module.exports = SuccessPage diff --git a/internal_packages/onboarding/package.json b/internal_packages/onboarding/package.json index cbbd09686..676e88b21 100755 --- a/internal_packages/onboarding/package.json +++ b/internal_packages/onboarding/package.json @@ -9,5 +9,8 @@ "atom": "*" }, "dependencies": { + }, + "windowTypes": { + "onboarding": true } } diff --git a/internal_packages/onboarding/spec/login-page-spec.cjsx b/internal_packages/onboarding/spec/login-page-spec.cjsx new file mode 100644 index 000000000..f051cc956 --- /dev/null +++ b/internal_packages/onboarding/spec/login-page-spec.cjsx @@ -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() + picker = ReactTestUtils.findRenderedDOMComponentWithClass(@loginPage, "environment-selector") + expect(picker).toBeDefined() + + it "hides env picker in other modes", -> + spyOn(atom, "inDevMode").andReturn false + @loginPage = ReactTestUtils.renderIntoDocument() + expect(-> ReactTestUtils.findRenderedDOMComponentWithClass(@loginPage, "environment-selector")).toThrow() + + it 'can change the environment', -> + spyOn(atom, "inDevMode").andReturn true + spyOn(OnboardingActions, "changeAPIEnvironment") + @loginPage = ReactTestUtils.renderIntoDocument() + 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() + + 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") diff --git a/internal_packages/onboarding/spec/nylas-api-environment-store-spec.coffee b/internal_packages/onboarding/spec/nylas-api-environment-store-spec.coffee new file mode 100644 index 000000000..c6da261b6 --- /dev/null +++ b/internal_packages/onboarding/spec/nylas-api-environment-store-spec.coffee @@ -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() diff --git a/internal_packages/onboarding/stylesheets/onboarding.less b/internal_packages/onboarding/stylesheets/onboarding.less index c7bc4f9f3..9efcbf6e9 100644 --- a/internal_packages/onboarding/stylesheets/onboarding.less +++ b/internal_packages/onboarding/stylesheets/onboarding.less @@ -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 { diff --git a/src/atom.coffee b/src/atom.coffee index d3d13bdda..647a8a1f4 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -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) diff --git a/src/browser/window-manager.coffee b/src/browser/window-manager.coffee index 5a857e226..8020e9c87 100644 --- a/src/browser/window-manager.coffee +++ b/src/browser/window-manager.coffee @@ -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] ?= [] diff --git a/src/flux/coffee-helpers.coffee b/src/flux/coffee-helpers.coffee index a5defd94e..54d20c874 100644 --- a/src/flux/coffee-helpers.coffee +++ b/src/flux/coffee-helpers.coffee @@ -1,6 +1,6 @@ _ = require 'underscore' -module.exports = +module.exports = CoffeeHelpers = # This copied out CoffeeScript includeModule: (mixin) -> if not mixin diff --git a/static/images/empty-state/Setup-Spinner.gif b/static/images/empty-state/Setup-Spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..558316475ba8a03292b7354ddce9e3575b7f45aa GIT binary patch literal 33997 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 literal 0 HcmV?d00001