diff --git a/core/client/app/components/gh-editor-save-button.js b/core/client/app/components/gh-editor-save-button.js index ea806697ab9e..5ddc2ca8dd38 100644 --- a/core/client/app/components/gh-editor-save-button.js +++ b/core/client/app/components/gh-editor-save-button.js @@ -9,6 +9,7 @@ export default Ember.Component.extend({ isPublished: null, willPublish: null, postOrPage: null, + submitting: false, // Tracks whether we're going to change the state of the post on save isDangerous: Ember.computed('isPublished', 'willPublish', function () { diff --git a/core/client/app/components/gh-spin-button.js b/core/client/app/components/gh-spin-button.js new file mode 100644 index 000000000000..ac9bb6f8d58c --- /dev/null +++ b/core/client/app/components/gh-spin-button.js @@ -0,0 +1,38 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: 'button', + buttonText: '', + submitting: false, + autoWidth: true, + + // Disable Button when isLoading equals true + attributeBindings: ['disabled'], + + // Must be set on the controller + disabled: Ember.computed.equal('submitting', true), + + click: function () { + if (this.get('action')) { + this.sendAction('action'); + return false; + } + return true; + }, + + setSize: function () { + if (!this.get('submitting') && this.get('autoWidth')) { + // this exists so that the spinner doesn't change the size of the button + this.$().width(this.$().width()); // sets width of button + this.$().height(this.$().height()); // sets height of button + } + }, + + width: Ember.observer('buttonText', 'autoWidth', function () { + this.setSize(); + }), + + didInsertElement: function () { + this.setSize(); + } +}); diff --git a/core/client/app/controllers/modals/signin.js b/core/client/app/controllers/modals/signin.js index ada9b10280f7..435199472964 100644 --- a/core/client/app/controllers/modals/signin.js +++ b/core/client/app/controllers/modals/signin.js @@ -3,6 +3,7 @@ import ValidationEngine from 'ghost/mixins/validation-engine'; export default Ember.Controller.extend(ValidationEngine, { validationType: 'signin', + submitting: false, application: Ember.inject.controller(), notifications: Ember.inject.service(), @@ -28,6 +29,7 @@ export default Ember.Controller.extend(ValidationEngine, { // it needs to be caught so it doesn't generate an exception in the console, // but it's actually "handled" by the sessionAuthenticationFailed action handler. }).finally(function () { + self.toggleProperty('submitting'); appController.set('skipAuthSuccessHandler', undefined); }); }, @@ -35,6 +37,8 @@ export default Ember.Controller.extend(ValidationEngine, { validateAndAuthenticate: function () { var self = this; + this.toggleProperty('submitting'); + // Manually trigger events for input fields, ensuring legacy compatibility with // browsers and password managers that don't send proper events on autofill $('#login').find('input').trigger('change'); diff --git a/core/client/app/controllers/settings/code-injection.js b/core/client/app/controllers/settings/code-injection.js index c68c63ee95ae..a197c4e1fbbd 100644 --- a/core/client/app/controllers/settings/code-injection.js +++ b/core/client/app/controllers/settings/code-injection.js @@ -1,17 +1,14 @@ import Ember from 'ember'; +import SettingsSaveMixin from 'ghost/mixins/settings-save'; -export default Ember.Controller.extend({ +export default Ember.Controller.extend(SettingsSaveMixin, { notifications: Ember.inject.service(), - actions: { - save: function () { - var notifications = this.get('notifications'); + save: function () { + var notifications = this.get('notifications'); - return this.get('model').save().then(function (model) { - return model; - }).catch(function (error) { - notifications.showAPIError(error); - }); - } + return this.get('model').save().catch(function (error) { + notifications.showAPIError(error); + }); } }); diff --git a/core/client/app/controllers/settings/general.js b/core/client/app/controllers/settings/general.js index 68a67779a936..cc57f5c21f56 100644 --- a/core/client/app/controllers/settings/general.js +++ b/core/client/app/controllers/settings/general.js @@ -1,7 +1,8 @@ import Ember from 'ember'; +import SettingsSaveMixin from 'ghost/mixins/settings-save'; import randomPassword from 'ghost/utils/random-password'; -export default Ember.Controller.extend({ +export default Ember.Controller.extend(SettingsSaveMixin, { notifications: Ember.inject.service(), config: Ember.inject.service(), @@ -62,24 +63,24 @@ export default Ember.Controller.extend({ } }), - actions: { - validate: function () { - this.get('model').validate(arguments); - }, + save: function () { + var notifications = this.get('notifications'), + config = this.get('config'); - save: function () { - var notifications = this.get('notifications'), - config = this.get('config'); + return this.get('model').save().then(function (model) { + config.set('blogTitle', model.get('title')); - return this.get('model').save().then(function (model) { - config.set('blogTitle', model.get('title')); + return model; + }).catch(function (error) { + if (error) { + notifications.showAPIError(error); + } + }); + }, - return model; - }).catch(function (error) { - if (error) { - notifications.showAPIError(error); - } - }); + actions: { + validate: function () { + this.get('model').validate(arguments); }, checkPostsPerPage: function () { diff --git a/core/client/app/controllers/settings/labs.js b/core/client/app/controllers/settings/labs.js index c1e362957272..4cd9869e9068 100644 --- a/core/client/app/controllers/settings/labs.js +++ b/core/client/app/controllers/settings/labs.js @@ -4,6 +4,7 @@ import {request as ajax} from 'ic-ajax'; export default Ember.Controller.extend({ uploadButtonText: 'Import', importErrors: '', + submitting: false, ghostPaths: Ember.inject.service('ghost-paths'), notifications: Ember.inject.service(), @@ -78,18 +79,23 @@ export default Ember.Controller.extend({ }, sendTestEmail: function () { - var notifications = this.get('notifications'); + var notifications = this.get('notifications'), + self = this; + + this.toggleProperty('submitting'); ajax(this.get('ghostPaths.url').api('mail', 'test'), { type: 'POST' }).then(function () { notifications.showAlert('Check your email for the test message.', {type: 'info'}); + self.toggleProperty('submitting'); }).catch(function (error) { if (typeof error.jqXHR !== 'undefined') { notifications.showAPIError(error); } else { notifications.showErrors(error); } + self.toggleProperty('submitting'); }); } } diff --git a/core/client/app/controllers/settings/navigation.js b/core/client/app/controllers/settings/navigation.js index 1807d0da6aa7..ff19c4ebcb82 100644 --- a/core/client/app/controllers/settings/navigation.js +++ b/core/client/app/controllers/settings/navigation.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import SettingsSaveMixin from 'ghost/mixins/settings-save'; var NavItem = Ember.Object.extend({ label: '', @@ -10,7 +11,7 @@ var NavItem = Ember.Object.extend({ }) }); -export default Ember.Controller.extend({ +export default Ember.Controller.extend(SettingsSaveMixin, { config: Ember.inject.service(), notifications: Ember.inject.service(), @@ -54,6 +55,65 @@ export default Ember.Controller.extend({ }); }), + save: function () { + var navSetting, + blogUrl = this.get('config').blogUrl, + blogUrlRegex = new RegExp('^' + blogUrl + '(.*)', 'i'), + navItems = this.get('navigationItems'), + message = 'One of your navigation items has an empty label. ' + + '
Please enter a new label or delete the item before saving.', + match, + notifications = this.get('notifications'); + + // Don't save if there's a blank label. + if (navItems.find(function (item) {return !item.get('isComplete') && !item.get('last');})) { + notifications.showAlert(message.htmlSafe(), {type: 'error'}); + return; + } + + navSetting = navItems.map(function (item) { + var label, + url; + + if (!item || !item.get('isComplete')) { + return; + } + + label = item.get('label').trim(); + url = item.get('url').trim(); + + // is this an internal URL? + match = url.match(blogUrlRegex); + + if (match) { + url = match[1]; + + // if the last char is not a slash, then add one, + // as long as there is no # or . in the URL (anchor or file extension) + // this also handles the empty case for the homepage + if (url[url.length - 1] !== '/' && url.indexOf('#') === -1 && url.indexOf('.') === -1) { + url += '/'; + } + } else if (!validator.isURL(url) && url !== '' && url[0] !== '/' && url.indexOf('mailto:') !== 0) { + url = '/' + url; + } + + return {label: label, url: url}; + }).compact(); + + this.set('model.navigation', JSON.stringify(navSetting)); + + // trigger change event because even if the final JSON is unchanged + // we need to have navigationItems recomputed. + this.get('model').notifyPropertyChange('navigation'); + + notifications.closeNotifications(); + + return this.get('model').save().catch(function (err) { + notifications.showErrors(err); + }); + }, + actions: { addItem: function () { var navItems = this.get('navigationItems'), @@ -94,65 +154,6 @@ export default Ember.Controller.extend({ } navItem.set('url', url); - }, - - save: function () { - var navSetting, - blogUrl = this.get('config').blogUrl, - blogUrlRegex = new RegExp('^' + blogUrl + '(.*)', 'i'), - navItems = this.get('navigationItems'), - message = 'One of your navigation items has an empty label. ' + - '
Please enter a new label or delete the item before saving.', - match, - notifications = this.get('notifications'); - - // Don't save if there's a blank label. - if (navItems.find(function (item) {return !item.get('isComplete') && !item.get('last');})) { - notifications.showAlert(message.htmlSafe(), {type: 'error'}); - return; - } - - navSetting = navItems.map(function (item) { - var label, - url; - - if (!item || !item.get('isComplete')) { - return; - } - - label = item.get('label').trim(); - url = item.get('url').trim(); - - // is this an internal URL? - match = url.match(blogUrlRegex); - - if (match) { - url = match[1]; - - // if the last char is not a slash, then add one, - // as long as there is no # or . in the URL (anchor or file extension) - // this also handles the empty case for the homepage - if (url[url.length - 1] !== '/' && url.indexOf('#') === -1 && url.indexOf('.') === -1) { - url += '/'; - } - } else if (!validator.isURL(url) && url !== '' && url[0] !== '/' && url.indexOf('mailto:') !== 0) { - url = '/' + url; - } - - return {label: label, url: url}; - }).compact(); - - this.set('model.navigation', JSON.stringify(navSetting)); - - // trigger change event because even if the final JSON is unchanged - // we need to have navigationItems recomputed. - this.get('model').notifyPropertyChange('navigation'); - - notifications.closeNotifications(); - - this.get('model').save().catch(function (err) { - notifications.showErrors(err); - }); } } }); diff --git a/core/client/app/controllers/setup/three.js b/core/client/app/controllers/setup/three.js index bd0ed227fdec..00510a51cc27 100644 --- a/core/client/app/controllers/setup/three.js +++ b/core/client/app/controllers/setup/three.js @@ -9,6 +9,7 @@ export default Ember.Controller.extend({ users: '', ownerEmail: Ember.computed.alias('two.email'), + submitting: false, usersArray: Ember.computed('users', function () { var users = this.get('users').split('\n').filter(function (email) { return email.trim().length > 0; @@ -75,6 +76,7 @@ export default Ember.Controller.extend({ this.get('errors').clear(); if (validationErrors === true && users.length > 0) { + this.toggleProperty('submitting'); this.get('authorRole').then(function (authorRole) { Ember.RSVP.Promise.all( users.map(function (user) { @@ -123,6 +125,8 @@ export default Ember.Controller.extend({ self.send('loadServerNotifications'); self.transitionTo('posts.index'); } + + self.toggleProperty('submitting'); }); }); } else if (users.length === 0) { diff --git a/core/client/app/controllers/setup/two.js b/core/client/app/controllers/setup/two.js index f703dd55a667..c506e723e1eb 100644 --- a/core/client/app/controllers/setup/two.js +++ b/core/client/app/controllers/setup/two.js @@ -11,6 +11,7 @@ export default Ember.Controller.extend(ValidationEngine, { password: null, image: null, blogCreated: false, + submitting: false, ghostPaths: Ember.inject.service('ghost-paths'), notifications: Ember.inject.service(), @@ -54,6 +55,8 @@ export default Ember.Controller.extend(ValidationEngine, { config = this.get('config'), method = this.get('blogCreated') ? 'PUT' : 'POST'; + this.toggleProperty('submitting'); + this.validate().then(function () { self.set('showError', false); ajax({ @@ -80,18 +83,23 @@ export default Ember.Controller.extend(ValidationEngine, { if (data.image) { self.sendImage(result.users[0]) .then(function () { + self.toggleProperty('submitting'); self.transitionToRoute('setup.three'); }).catch(function (resp) { + self.toggleProperty('submitting'); notifications.showAPIError(resp); }); } else { + self.toggleProperty('submitting'); self.transitionToRoute('setup.three'); } }); }).catch(function (resp) { + self.toggleProperty('submitting'); notifications.showAPIError(resp); }); }).catch(function () { + self.toggleProperty('submitting'); self.set('showError', true); }); }, diff --git a/core/client/app/controllers/signin.js b/core/client/app/controllers/signin.js index 6a0cad34b8fe..20f084e67526 100644 --- a/core/client/app/controllers/signin.js +++ b/core/client/app/controllers/signin.js @@ -4,6 +4,7 @@ import {request as ajax} from 'ic-ajax'; export default Ember.Controller.extend(ValidationEngine, { submitting: false, + loggingIn: false, ghostPaths: Ember.inject.service('ghost-paths'), notifications: Ember.inject.service(), @@ -19,12 +20,15 @@ export default Ember.Controller.extend(ValidationEngine, { authStrategy = 'simple-auth-authenticator:oauth2-password-grant', data = model.getProperties('identification', 'password'); - this.get('session').authenticate(authStrategy, data).catch(function (err) { + this.get('session').authenticate(authStrategy, data).then(function () { + self.toggleProperty('loggingIn'); + }).catch(function (err) { + self.toggleProperty('loggingIn'); + if (err.errors) { self.set('flowErrors', err.errors[0].message.string); } - - // If authentication fails a rejected promise will be returned. + // if authentication fails a rejected promise will be returned. // it needs to be caught so it doesn't generate an exception in the console, // but it's actually "handled" by the sessionAuthenticationFailed action handler. }); @@ -39,6 +43,7 @@ export default Ember.Controller.extend(ValidationEngine, { this.validate().then(function () { self.get('notifications').closeNotifications(); + self.toggleProperty('loggingIn'); self.send('authenticate'); }).catch(function (error) { if (error) { @@ -56,7 +61,7 @@ export default Ember.Controller.extend(ValidationEngine, { this.set('flowErrors', ''); this.validate({property: 'identification'}).then(function () { - self.set('submitting', true); + self.toggleProperty('submitting'); ajax({ url: self.get('ghostPaths.url').api('authentication', 'passwordreset'), @@ -67,10 +72,10 @@ export default Ember.Controller.extend(ValidationEngine, { }] } }).then(function () { - self.set('submitting', false); + self.toggleProperty('submitting'); notifications.showAlert('Please check your email for instructions.', {type: 'info'}); }).catch(function (resp) { - self.set('submitting', false); + self.toggleProperty('submitting'); if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) { self.set('flowErrors', resp.jqXHR.responseJSON.errors[0].message); } else { diff --git a/core/client/app/controllers/team/user.js b/core/client/app/controllers/team/user.js index 0598d2bcf425..a52aaed442dd 100644 --- a/core/client/app/controllers/team/user.js +++ b/core/client/app/controllers/team/user.js @@ -7,6 +7,7 @@ import ValidationEngine from 'ghost/mixins/validation-engine'; export default Ember.Controller.extend(ValidationEngine, { // ValidationEngine settings validationType: 'user', + submitting: false, ghostPaths: Ember.inject.service('ghost-paths'), notifications: Ember.inject.service(), @@ -103,6 +104,8 @@ export default Ember.Controller.extend(ValidationEngine, { user.set('slug', slugValue); } + this.toggleProperty('submitting'); + promise = Ember.RSVP.resolve(afterUpdateSlug).then(function () { return user.save({format: false}); }).then(function (model) { @@ -121,11 +124,15 @@ export default Ember.Controller.extend(ValidationEngine, { window.history.replaceState({path: newPath}, '', newPath); } + this.toggleProperty('submitting'); + return model; }).catch(function (errors) { if (errors) { self.get('notifications').showErrors(errors); } + + this.toggleProperty('submitting'); }); this.set('lastPromise', promise); diff --git a/core/client/app/mixins/editor-base-controller.js b/core/client/app/mixins/editor-base-controller.js index 7fa44730f4af..bb1b5a21f4f1 100644 --- a/core/client/app/mixins/editor-base-controller.js +++ b/core/client/app/mixins/editor-base-controller.js @@ -17,6 +17,7 @@ export default Ember.Mixin.create({ autoSaveId: null, timedSaveId: null, editor: null, + submitting: false, notifications: Ember.inject.service(), @@ -247,6 +248,8 @@ export default Ember.Mixin.create({ options = options || {}; + this.toggleProperty('submitting'); + if (options.backgroundSave) { // do not allow a post's status to be set to published by a background save status = 'draft'; @@ -294,6 +297,7 @@ export default Ember.Mixin.create({ self.showSaveNotification(prevStatus, model.get('status'), isNew ? true : false); } + self.toggleProperty('submitting'); return model; }); }).catch(function (errors) { @@ -303,6 +307,7 @@ export default Ember.Mixin.create({ self.set('model.status', prevStatus); + self.toggleProperty('submitting'); return self.get('model'); }); diff --git a/core/client/app/mixins/settings-save.js b/core/client/app/mixins/settings-save.js new file mode 100644 index 000000000000..7341a142b154 --- /dev/null +++ b/core/client/app/mixins/settings-save.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; + +export default Ember.Mixin.create({ + submitting: false, + + actions: { + save: function () { + var self = this; + + this.set('submitting', true); + + this.save().then(function () { + self.set('submitting', false); + }); + } + } +}); diff --git a/core/client/app/styles/patterns/buttons.css b/core/client/app/styles/patterns/buttons.css index e17b484948f2..232d90257413 100644 --- a/core/client/app/styles/patterns/buttons.css +++ b/core/client/app/styles/patterns/buttons.css @@ -200,3 +200,27 @@ input[type="reset"].btn-block, input[type="button"].btn-block { width: 100%; } + +/* Spin Buttons! +/* ---------------------------------------------------------- */ +.spinner { + position: relative; + display: inline-block; + box-sizing: border-box; + margin: -2px 0; + width: 14px; + height: 14px; + border: rgba(0,0,0,0.2) solid 4px; + border-radius: 100px; + animation: spin 1s linear infinite; +} + +.spinner:before { + content: ""; + display: block; + margin-top: 6px; + width: 4px; + height: 4px; + background: rgba(0,0,0,0.6); + border-radius: 100px; +} diff --git a/core/client/app/styles/patterns/global.css b/core/client/app/styles/patterns/global.css index a74c823db950..2a5ddd9e9410 100644 --- a/core/client/app/styles/patterns/global.css +++ b/core/client/app/styles/patterns/global.css @@ -406,6 +406,15 @@ img { } } +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + .fade-in { animation: fade-in 0.2s; animation-fill-mode: forwards; diff --git a/core/client/app/templates/components/gh-editor-save-button.hbs b/core/client/app/templates/components/gh-editor-save-button.hbs index 51af0661dbb2..7d938500b651 100644 --- a/core/client/app/templates/components/gh-editor-save-button.hbs +++ b/core/client/app/templates/components/gh-editor-save-button.hbs @@ -1,6 +1,4 @@ - +{{gh-spin-button type="button" classNameBindings=":btn :btn-sm :js-publish-button isDangerous:btn-red:btn-blue" action="save" buttonText=saveText submitting=submitting}} {{#gh-dropdown-button dropdownName="post-save-menu" classNameBindings=":btn :btn-sm isDangerous:btn-red:btn-blue btnopen:active :dropdown-toggle :up"}} diff --git a/core/client/app/templates/components/gh-spin-button.hbs b/core/client/app/templates/components/gh-spin-button.hbs new file mode 100644 index 000000000000..a18aecf6da39 --- /dev/null +++ b/core/client/app/templates/components/gh-spin-button.hbs @@ -0,0 +1,9 @@ +{{#unless submitting}} + {{#if buttonText}} + {{buttonText}} + {{else}} + {{{yield}}} + {{/if}} +{{else}} + +{{/unless}} diff --git a/core/client/app/templates/editor/edit.hbs b/core/client/app/templates/editor/edit.hbs index 45b1867c2145..ac9b51263c98 100644 --- a/core/client/app/templates/editor/edit.hbs +++ b/core/client/app/templates/editor/edit.hbs @@ -16,6 +16,7 @@ save="save" setSaveType="setSaveType" delete="openDeleteModal" + submitting=submitting }} diff --git a/core/client/app/templates/modals/signin.hbs b/core/client/app/templates/modals/signin.hbs index e33ce0124ca0..e60fb696a719 100644 --- a/core/client/app/templates/modals/signin.hbs +++ b/core/client/app/templates/modals/signin.hbs @@ -5,7 +5,7 @@
{{input class="gh-input password" type="password" placeholder="Password" name="password" value=password}}
- + {{gh-spin-button class="btn btn-blue" type="submit" action="validateAndAuthenticate" submitting=submitting buttonText="Log in"}} {{/gh-modal-dialog}} diff --git a/core/client/app/templates/reset.hbs b/core/client/app/templates/reset.hbs index b630b5e53213..f7c112d740f0 100644 --- a/core/client/app/templates/reset.hbs +++ b/core/client/app/templates/reset.hbs @@ -17,7 +17,7 @@ {{input value=ne2Password class="gh-input password" type="password" placeholder="Confirm Password" name="ne2password" focusOut=(action "validate" "ne2Password")}} {{gh-error-message errors=errors property="ne2Password"}} {{/gh-form-group}} - + {{gh-spin-button class="btn btn-blue btn-block" type="submit" submitting=submitting buttonText="Reset Password" autoWidth=false}} diff --git a/core/client/app/templates/settings/code-injection.hbs b/core/client/app/templates/settings/code-injection.hbs index e99f2ad87f56..de71df50d4e3 100644 --- a/core/client/app/templates/settings/code-injection.hbs +++ b/core/client/app/templates/settings/code-injection.hbs @@ -2,7 +2,7 @@
{{#gh-view-title openMobileMenu="openMobileMenu"}}Code Injection{{/gh-view-title}}
- + {{gh-spin-button type="button" class="btn btn-blue" action="save" buttonText="Save" submitting=submitting}}
diff --git a/core/client/app/templates/settings/general.hbs b/core/client/app/templates/settings/general.hbs index 5ea6b519602f..a9aa94fcd1e8 100644 --- a/core/client/app/templates/settings/general.hbs +++ b/core/client/app/templates/settings/general.hbs @@ -2,7 +2,7 @@
{{#gh-view-title openMobileMenu="openMobileMenu"}}General{{/gh-view-title}}
- + {{gh-spin-button type="button" class="btn btn-blue" action="save" buttonText="Save" submitting=submitting}}
diff --git a/core/client/app/templates/settings/labs.hbs b/core/client/app/templates/settings/labs.hbs index e3606de8d784..f5fe3dab79e9 100644 --- a/core/client/app/templates/settings/labs.hbs +++ b/core/client/app/templates/settings/labs.hbs @@ -37,7 +37,7 @@
- + {{gh-spin-button type="button" id="sendtestemail" class="btn btn-blue" action="sendTestEmail" buttonText="Send" submitting=submitting}}

Sends a test email to your address.

diff --git a/core/client/app/templates/settings/navigation.hbs b/core/client/app/templates/settings/navigation.hbs index 4bec4cf02c1f..488973201b73 100644 --- a/core/client/app/templates/settings/navigation.hbs +++ b/core/client/app/templates/settings/navigation.hbs @@ -2,7 +2,7 @@
{{#gh-view-title openMobileMenu="openMobileMenu"}}Navigation{{/gh-view-title}}
- + {{gh-spin-button type="button" class="btn btn-blue" action="save" buttonText="Save" submitting=submitting}}
diff --git a/core/client/app/templates/setup/three.hbs b/core/client/app/templates/setup/three.hbs index f67d5922d07a..ef190ed6bb6f 100644 --- a/core/client/app/templates/setup/three.hbs +++ b/core/client/app/templates/setup/three.hbs @@ -11,9 +11,8 @@ {{gh-error-message errors=errors property="users"}} - +{{gh-spin-button type="button" classNameBindings=":btn :btn-default :btn-lg :btn-block buttonClass" action="invite" buttonText=buttonText submitting=submitting autoWidth=false}} + diff --git a/core/client/app/templates/setup/two.hbs b/core/client/app/templates/setup/two.hbs index 740e299e31d4..ba333643afec 100644 --- a/core/client/app/templates/setup/two.hbs +++ b/core/client/app/templates/setup/two.hbs @@ -37,6 +37,7 @@ {{gh-error-message errors=errors property="blogTitle"}} {{/gh-form-group}} - - +{{#gh-spin-button type="button" class="btn btn-green btn-lg btn-block" action="setup" submitting=submitting autoWidth=false}} + Last step: Invite your team +{{/gh-spin-button}}

{{#if showError}}{{invalidMessage}}{{/if}}

diff --git a/core/client/app/templates/signin.hbs b/core/client/app/templates/signin.hbs index 2f31a328673f..8a2dd576b261 100644 --- a/core/client/app/templates/signin.hbs +++ b/core/client/app/templates/signin.hbs @@ -15,7 +15,7 @@ {{gh-error-message errors=model.errors property="password"}} {{/gh-form-group}} - + {{gh-spin-button class="login btn btn-blue btn-block" type="submit" tabindex="3" buttonText="Sign in" submitting=loggingIn autoWidth=false}}

{{{flowErrors}}}

diff --git a/core/client/app/templates/signup.hbs b/core/client/app/templates/signup.hbs index de601d81c8e2..346ddaa7c006 100644 --- a/core/client/app/templates/signup.hbs +++ b/core/client/app/templates/signup.hbs @@ -48,7 +48,7 @@ {{/gh-form-group}} - + {{gh-spin-button type="submit" class="btn btn-green btn-lg btn-block" action="signup" submitting=submitting autoWidth=false}} diff --git a/core/client/app/templates/team/user.hbs b/core/client/app/templates/team/user.hbs index 24ffd7639218..420bb0f4f41b 100644 --- a/core/client/app/templates/team/user.hbs +++ b/core/client/app/templates/team/user.hbs @@ -30,7 +30,7 @@ {{/if}} - + {{gh-spin-button class="btn btn-blue" action="save" buttonText="Save" submitting=submitting}} diff --git a/core/client/tests/unit/components/gh-editor-save-button-test.js b/core/client/tests/unit/components/gh-editor-save-button-test.js index 85cc2b905d65..fb91697e33d9 100644 --- a/core/client/tests/unit/components/gh-editor-save-button-test.js +++ b/core/client/tests/unit/components/gh-editor-save-button-test.js @@ -12,6 +12,7 @@ describeComponent( needs: [ 'component:gh-dropdown-button', 'component:gh-dropdown', + 'component:gh-spin-button', 'service:dropdown' ] }, diff --git a/core/client/tests/unit/components/gh-spin-button-test.js b/core/client/tests/unit/components/gh-spin-button-test.js new file mode 100644 index 000000000000..33fb3c167e25 --- /dev/null +++ b/core/client/tests/unit/components/gh-spin-button-test.js @@ -0,0 +1,27 @@ +/* jshint expr:true */ +import {expect} from 'chai'; +import { + describeComponent, + it +} from 'ember-mocha'; + +describeComponent( + 'gh-spin-button', + 'GhSpinButtonComponent', + { + // specify the other units that are required for this test + // needs: ['component:foo', 'helper:bar'] + }, + function () { + it('renders', function () { + // creates the component instance + var component = this.subject(); + + expect(component._state).to.equal('preRender'); + + // renders the component on the page + this.render(); + expect(component._state).to.equal('inDOM'); + }); + } +); diff --git a/core/test/functional/client/editor_test.js b/core/test/functional/client/editor_test.js index dc4df0a0e843..6e4c18221e7e 100644 --- a/core/test/functional/client/editor_test.js +++ b/core/test/functional/client/editor_test.js @@ -273,13 +273,13 @@ CasperTest.begin('Publish menu - new post', 10, function suite(test) { // Open the publish options menu; casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open'); + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu'); // Select the publish post button casper.thenClick('.js-publish-splitbutton li:first-child a'); // ... check status, label, class - casper.waitForSelector('.js-publish-splitbutton', function onSuccess() { + casper.waitForSelector('.js-publish-splitbutton .js-publish-button:not([disabled])', function onSuccess() { test.assertExists('.js-publish-button.btn-red', 'Publish button should have .btn-red'); test.assertSelectorHasText('.js-publish-button', 'Publish Now', '.js-publish-button says Publish Now'); }, function onTimeout() { @@ -291,7 +291,7 @@ CasperTest.begin('Publish menu - new post', 10, function suite(test) { casper.thenClick('.js-publish-button'); // ... check status, label, class - casper.waitForSelector('.js-publish-splitbutton', function onSuccess() { + casper.waitForSelector('.js-publish-splitbutton .js-publish-button:not([disabled])', function onSuccess() { test.assertExists('.js-publish-button.btn-blue', 'Update button should have .btn-blue'); test.assertSelectorHasText('.js-publish-button', 'Update Post', '.js-publish-button says Update Post'); }, function onTimeout() { @@ -329,13 +329,13 @@ CasperTest.begin('Publish menu - new page', 10, function suite(test) { // Open the publish options menu; casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open'); + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu'); // Select the publish post button casper.thenClick('.js-publish-splitbutton li:first-child a'); // ... check status, label, class - casper.waitForSelector('.js-publish-splitbutton', function onSuccess() { + casper.waitForSelector('.js-publish-splitbutton .js-publish-button:not([disabled])', function onSuccess() { test.assertExists('.js-publish-button.btn-red', 'Publish button should have .btn-red'); test.assertSelectorHasText('.js-publish-button', 'Publish Now', '.js-publish-button says Publish Now'); }, function onTimeout() { @@ -347,7 +347,7 @@ CasperTest.begin('Publish menu - new page', 10, function suite(test) { casper.thenClick('.js-publish-button'); // ... check status, label, class - casper.waitForSelector('.js-publish-splitbutton', function onSuccess() { + casper.waitForSelector('.js-publish-splitbutton .js-publish-button:not([disabled])', function onSuccess() { test.assertExists('.js-publish-button.btn-blue', 'Update button should have .btn-blue'); test.assertSelectorHasText('.js-publish-button', 'Update Page', '.js-publish-button says Update Page'); }, function onTimeout() { @@ -399,7 +399,7 @@ CasperTest.begin('Publish menu - existing post', 23, function suite(test) { casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open', function onSuccess() { + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu', function onSuccess() { test.assert(true, 'delete post button should be visible for saved drafts'); test.assertVisible( '.js-publish-splitbutton .delete', 'delete post button should be visible on saved drafts' @@ -412,7 +412,7 @@ CasperTest.begin('Publish menu - existing post', 23, function suite(test) { // Open the publish options menu; casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open'); + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu'); // Select the publish post button casper.thenClick('.js-publish-splitbutton li:first-child a'); @@ -445,7 +445,7 @@ CasperTest.begin('Publish menu - existing post', 23, function suite(test) { // Open the publish options menu; casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open'); + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu'); // Select the publish post button casper.thenClick('.js-publish-splitbutton li:nth-child(2) a'); @@ -488,7 +488,7 @@ CasperTest.begin('Publish menu - delete post', 6, function testDeleteModal(test) // Open post settings menu casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open'); + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu'); casper.thenClick('.js-publish-splitbutton li:nth-child(4) a'); casper.waitUntilVisible('.modal-container', function onSuccess() { @@ -506,7 +506,7 @@ CasperTest.begin('Publish menu - delete post', 6, function testDeleteModal(test) // Test delete casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open'); + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu'); casper.thenClick('.js-publish-splitbutton li:nth-child(4) a'); casper.waitForSelector('.modal-container .modal-content', function onSuccess() { @@ -536,7 +536,7 @@ CasperTest.begin('Publish menu - new post status is correct after failed save', // Open the publish options menu; casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open'); + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu'); // Select the publish post button casper.thenClick('.js-publish-splitbutton li:first-child a'); @@ -581,7 +581,7 @@ CasperTest.begin('Publish menu - existing post status is correct after failed sa // Open the publish options menu; casper.thenClick('.js-publish-splitbutton .dropdown-toggle'); - casper.waitForOpaque('.js-publish-splitbutton .open'); + casper.waitForOpaque('.js-publish-splitbutton .dropdown-menu'); // Select the publish post button casper.thenClick('.js-publish-splitbutton li:first-child a');