diff --git a/client/components/button/style.scss b/client/components/button/style.scss index 122fa9443d36..576125e8e3a6 100644 --- a/client/components/button/style.scss +++ b/client/components/button/style.scss @@ -102,7 +102,7 @@ button { } // Primary buttons -.button.is-primary { +@mixin button-is-primary { background: $blue-medium; border-color: $blue-wordpress; color: $white; @@ -123,6 +123,10 @@ button { } } +.button.is-primary { + @include button-is-primary; +} + // Scary buttons .button.is-scary { color: $alert-red; diff --git a/client/post-editor/editor-ground-control/index.jsx b/client/post-editor/editor-ground-control/index.jsx index 8e962028b08a..f64b457ea5e4 100644 --- a/client/post-editor/editor-ground-control/index.jsx +++ b/client/post-editor/editor-ground-control/index.jsx @@ -24,6 +24,7 @@ const Card = require( 'components/card' ), PostListFetcher = require( 'components/post-list-fetcher' ), stats = require( 'lib/posts/stats' ); import { setDate } from 'state/ui/editor/post/actions'; +import EditorPublishButton from 'post-editor/editor-publish-button'; const EditorGroundControl = React.createClass( { displayName: 'EditorGroundControl', @@ -33,9 +34,9 @@ const EditorGroundControl = React.createClass( { isSaveBlocked: React.PropTypes.bool, isPublishing: React.PropTypes.bool, isSaving: React.PropTypes.bool, - onClose: React.PropTypes.func, onPreview: React.PropTypes.func, onPublish: React.PropTypes.func, + onSave: React.PropTypes.func, onSaveDraft: React.PropTypes.func, post: React.PropTypes.object, setDate: React.PropTypes.func, @@ -53,12 +54,11 @@ const EditorGroundControl = React.createClass( { isSaveBlocked: false, isPublishing: false, isSaving: false, - onClose: noop, onPublish: noop, onSaveDraft: noop, post: null, savedPost: null, - setDate: () => {}, + setDate: noop, site: {} }; }, @@ -94,65 +94,6 @@ const EditorGroundControl = React.createClass( { return this.translate( 'Preview' ); }, - trackPrimaryButton: function() { - const postEvents = { - update: 'Clicked Update Post Button', - schedule: 'Clicked Schedule Post Button', - requestReview: 'Clicked Request-Review Post Button', - publish: 'Clicked Publish Post Button', - }; - const pageEvents = { - update: 'Clicked Update Page Button', - schedule: 'Clicked Schedule Page Button', - requestReview: 'Clicked Request-Review Page Button', - publish: 'Clicked Publish Page Button', - }; - const buttonState = this.getPrimaryButtonState(); - const eventString = postUtils.isPage( this.props.post ) ? pageEvents[ buttonState ] : postEvents[ buttonState ]; - stats.recordEvent( eventString ); - stats.recordEvent( 'Clicked Primary Button' ); - }, - - getPrimaryButtonState: function() { - if ( - postUtils.isPublished( this.props.savedPost ) && - ! postUtils.isBackDatedPublished( this.props.savedPost ) && - ! postUtils.isFutureDated( this.props.post ) || - ( - this.props.savedPost && - this.props.savedPost.status === 'future' && - postUtils.isFutureDated( this.props.post ) - ) - ) { - return 'update'; - } - - if ( postUtils.isFutureDated( this.props.post ) ) { - return 'schedule'; - } - - if ( siteUtils.userCan( 'publish_posts', this.props.site ) ) { - return 'publish'; - } - - if ( this.props.savedPost && this.props.savedPost.status === 'pending' ) { - return 'update'; - } - - return 'requestReview'; - }, - - getPrimaryButtonLabel: function() { - const primaryButtonState = this.getPrimaryButtonState(); - const buttonLabels = { - update: this.translate( 'Update' ), - schedule: this.translate( 'Schedule' ), - publish: this.translate( 'Publish' ), - requestReview: this.translate( 'Submit for Review' ), - }; - return buttonLabels[ primaryButtonState ]; - }, - toggleSchedulePopover: function() { this.setState( { showSchedulePopover: ! this.state.showSchedulePopover } ); }, @@ -251,36 +192,14 @@ const EditorGroundControl = React.createClass( { ! this.props.isSaveBlocked; }, - isPrimaryButtonEnabled: function() { - return ! this.props.isPublishing && - ! this.props.isSaveBlocked && - this.props.hasContent; + canPublishPost: function() { + return siteUtils.userCan( 'publish_posts', this.props.site ); }, toggleAdvancedStatus: function() { this.setState( { showAdvanceStatus: ! this.state.showAdvanceStatus } ); }, - onPrimaryButtonClick: function() { - this.trackPrimaryButton(); - - if ( postUtils.isFutureDated( this.props.post ) ) { - return this.props.onSave( 'future' ); - } - - if ( postUtils.isPublished( this.props.savedPost ) && - ! postUtils.isBackDatedPublished( this.props.savedPost ) - ) { - return this.props.onSave(); - } - - if ( siteUtils.userCan( 'publish_posts', this.props.site ) ) { - return this.props.onPublish(); - } - - return this.props.onSave( 'pending' ); - }, - onSaveButtonClick: function() { this.props.onSave(); const eventLabel = postUtils.isPage( this.props.page ) ? 'Clicked Save Page Button' : 'Clicked Save Post Button'; @@ -373,18 +292,21 @@ const EditorGroundControl = React.createClass( { { this.getPreviewLabel() }
- - { siteUtils.userCan( 'publish_posts', this.props.site ) && + isPublishing={ this.props.isPublishing } + isSaveBlocked={ this.props.isSaveBlocked } + hasContent={ this.props.hasContent } + /> + { this.canPublishPost() && } { this.renderDateTooltip() }
- { siteUtils.userCan( 'publish_posts', this.props.site ) && + { this.canPublishPost() && this.schedulePostPopover() } diff --git a/client/post-editor/editor-ground-control/style.scss b/client/post-editor/editor-ground-control/style.scss index c8b8109b374c..d98b4500bf19 100644 --- a/client/post-editor/editor-ground-control/style.scss +++ b/client/post-editor/editor-ground-control/style.scss @@ -38,15 +38,36 @@ margin: 0 4px 8px; } -.editor-ground-control__publish-button { +.editor-ground-control .editor-publish-button { border-radius: 4px 0 0 4px; flex-grow: 1; + + @include breakpoint( "<660px" ) { + display: none; + } } + .editor-ground-control__time-button { - border-radius: 0 4px 4px 0; + @include breakpoint( ">660px" ) { + @include button-is-primary; + + border-radius: 0 4px 4px 0; + padding-left: 8px; + padding-right: 8px; + margin-left: -1px; + } + + @include breakpoint( "<660px" ) { + width: 100%; + } +} + +.editor-ground-control__time-button__label { padding-left: 8px; - padding-right: 8px; - margin-left: -1px; + + @include breakpoint( ">660px" ) { + display: none; + } } .editor-ground-control__save.button.is-link, diff --git a/client/post-editor/editor-ground-control/test/index.jsx b/client/post-editor/editor-ground-control/test/index.jsx index fd204760f1d1..665211997764 100644 --- a/client/post-editor/editor-ground-control/test/index.jsx +++ b/client/post-editor/editor-ground-control/test/index.jsx @@ -2,9 +2,7 @@ * External dependencies */ import { expect } from 'chai'; -import moment from 'moment'; import React from 'react'; -import sinon from 'sinon'; import mockery from 'mockery'; /** @@ -79,154 +77,6 @@ describe( 'EditorGroundControl', function() { } ); } ); - describe( '#getPrimaryButtonLabel()', function() { - it( 'should return Update if the post was originally published and is still slated to be published', function() { - var tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Update' ); - } ); - - it( 'should return Update if the post was originally published and is currently reverted to non-published status', function() { - var tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Update' ); - } ); - - it( 'should return Schedule if the post is dated in the future and not scheduled', function() { - var now = moment( new Date() ), - nextMonth = now.month( now.month() + 1 ).format(), - tree; - - tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Schedule' ); - } ); - - it( 'should return Schedule if the post is dated in the future and published', function() { - var now = moment( new Date() ), - nextMonth = now.month( now.month() + 1 ).format(), - tree; - - tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Schedule' ); - } ); - - it( 'should return Update if the post is scheduled and dated in the future', function() { - var now = moment( new Date() ), - nextMonth = now.month( now.month() + 1 ).format(), - tree; - - tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Update' ); - } ); - - it( 'should return Update if the post is scheduled, dated in the future, and next status is draft', function() { - var now = moment( new Date() ), - nextMonth = now.month( now.month() + 1 ).format(), - tree; - - tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Update' ); - } ); - - it( 'should return Publish if the post is scheduled and dated in the past', function() { - var now = moment( new Date() ), - lastMonth = now.month( now.month() - 1 ).format(), - tree; - - tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Publish' ); - } ); - - it( 'should return Update if the post was originally published and is scheduled with date in the past', function() { - var now = moment(), - lastMonth = now.month( now.month() - 1 ).format(), - tree; - - tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Update' ); - } ); - - it( 'should return Publish if the post is a draft', function() { - var tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Publish' ); - } ); - - it( 'should return "Submit for Review" if the post is a draft and user can\'t publish', function() { - var tree = shallow( - - ).instance(); - - expect( tree.getPrimaryButtonLabel() ).to.equal( 'Submit for Review' ); - } ); - } ); - describe( '#isSaveEnabled()', function() { it( 'should return false if form is saving', function() { var tree = shallow( ).instance(); @@ -296,180 +146,4 @@ describe( 'EditorGroundControl', function() { expect( tree.isPreviewEnabled() ).to.be.false; } ); } ); - - describe( '#isPrimaryButtonEnabled()', function() { - it( 'should return true if form is not publishing and post is not empty', function() { - var tree = shallow( ).instance(); - - expect( tree.isPrimaryButtonEnabled() ).to.be.true; - } ); - - it( 'should return true if form is not publishind and post is new and has content, but is not dirty', function() { - var tree = shallow( ).instance(); - - expect( tree.isPrimaryButtonEnabled() ).to.be.true; - } ); - - it( 'should return false if form is publishing', function() { - var tree = shallow( ).instance(); - - expect( tree.isPrimaryButtonEnabled() ).to.be.false; - } ); - - it( 'should return false if saving is blocked', function() { - var tree = shallow( ).instance(); - - expect( tree.isPrimaryButtonEnabled() ).to.be.false; - } ); - - it( 'should return false if not dirty and has no content', function() { - var tree = shallow( ).instance(); - - expect( tree.isPrimaryButtonEnabled() ).to.be.false; - } ); - - it( 'should return false if post has no content', function() { - var tree = shallow( ).instance(); - - expect( tree.isPrimaryButtonEnabled() ).to.be.false; - } ); - } ); - - describe( '#onPrimaryButtonClick', function() { - it( 'should publish a draft', function() { - var onPublish = sinon.spy(), - tree; - - tree = shallow( - - ).instance(); - - tree.onPrimaryButtonClick(); - - expect( onPublish ).to.have.been.called; - } ); - - it( 'should schedule a posted dated in future', function() { - var now = moment( new Date() ), - nextMonth = now.month( now.month() + 1 ).format(), - onSave = sinon.spy(), - tree; - - tree = shallow( - - ).instance(); - - tree.onPrimaryButtonClick(); - - expect( onSave ).to.have.been.calledWith( 'future' ); - } ); - - it( 'should save a scheduled post dated in future', function() { - var now = moment( new Date() ), - nextMonth = now.month( now.month() + 1 ).format(), - onSave = sinon.spy(), - tree; - - tree = shallow( - - ).instance(); - - tree.onPrimaryButtonClick(); - - expect( onSave ).to.have.been.calledWith( 'future' ); - } ); - - it( 'should publish a scheduled post dated in past', function() { - var now = moment( new Date() ), - lastMonth = now.month( now.month() - 1 ).format(), - onPublish = sinon.spy(), - tree; - - tree = shallow( - - ).instance(); - - tree.onPrimaryButtonClick(); - - expect( onPublish ).to.have.been.called; - } ); - - it( 'should update a published post that has changed status', function() { - var onSave = sinon.spy(), - tree; - - tree = shallow( - - ).instance(); - - tree.onPrimaryButtonClick(); - - expect( onSave ).to.have.been.called; - } ); - - it( 'should update a published post scheduled in the past', function() { - var now = moment( new Date() ), - lastMonth = now.month( now.month() - 1 ).format(), - onSave = sinon.spy(), - tree; - - tree = shallow( - - ).instance(); - - tree.onPrimaryButtonClick(); - - expect( onSave ).to.have.been.called; - } ); - - it( 'should set status to "pending" if the user can\'t publish', function() { - var onSave = sinon.spy(), - tree; - - tree = shallow( - - ).instance(); - - tree.onPrimaryButtonClick(); - - expect( onSave ).to.have.been.calledWith( 'pending' ); - } ); - } ); } ); diff --git a/client/post-editor/editor-mobile-navigation/index.jsx b/client/post-editor/editor-mobile-navigation/index.jsx index d82f4450497f..151c1baee4a2 100644 --- a/client/post-editor/editor-mobile-navigation/index.jsx +++ b/client/post-editor/editor-mobile-navigation/index.jsx @@ -1,19 +1,42 @@ /** * External dependencies */ -const React = require( 'react' ); +import React, { PropTypes } from 'react'; +import classnames from 'classnames'; /** * Internal dependencies */ -const Site = require( 'my-sites/site' ), - Gridicon = require( 'components/gridicon' ), - layoutFocus = require( 'lib/layout-focus' ); +import Gridicon from 'components/gridicon'; +import EditorPublishButton from 'post-editor/editor-publish-button'; +import Button from 'components/button'; +import observe from 'lib/mixins/data-observe'; -const EditorMobileNavigation = React.createClass( { +export default React.createClass( { + displayName: 'EditorMobileNavigation', - toggleSidebar: function() { - layoutFocus.set( 'sidebar' ); + mixins: [ observe( 'layoutFocus' ) ], + + propTypes: { + site: PropTypes.object, + post: PropTypes.object, + savedPost: PropTypes.object, + onSave: PropTypes.func, + onPublish: PropTypes.func, + tabIndex: PropTypes.number, + isPublishing: PropTypes.bool, + isSaveBlocked: PropTypes.bool, + hasContent: PropTypes.bool, + onClose: PropTypes.func, + layoutFocus: PropTypes.object + }, + + openSidebar: function() { + this.props.layoutFocus.set( 'sidebar' ); + }, + + closeSidebar: function() { + this.props.layoutFocus.set( 'content' ); }, render: function() { @@ -23,17 +46,40 @@ const EditorMobileNavigation = React.createClass( { return (
- - - +
+ +
+ + +
+
+
); } } ); - -module.exports = EditorMobileNavigation; diff --git a/client/post-editor/editor-mobile-navigation/style.scss b/client/post-editor/editor-mobile-navigation/style.scss index 05ac6f0e8546..b5a80dddad2c 100644 --- a/client/post-editor/editor-mobile-navigation/style.scss +++ b/client/post-editor/editor-mobile-navigation/style.scss @@ -1,42 +1,41 @@ .editor-mobile-navigation { - color: $gray; display: flex; justify-content: space-between; align-items: center; - background-color: $gray-light; + background-color: $white; + border-bottom: 1px lighten( $gray, 20% ) solid; @include breakpoint( ">660px" ) { display: none; } } -.editor-mobile-navigation__close { - cursor: pointer; - margin-right: 4px; - padding: 8px; +.editor-mobile-navigation__actions { + display: inline; + line-height: 0; +} - &:hover { - cursor: button; - fill: $gray-dark; - } +.editor-mobile-navigation__tabs { + display: inline-block; + border-left: 1px lighten( $gray, 20% ) solid; + border-right: 1px lighten( $gray, 20% ) solid; } -.editor-mobile-navigation .site { - .site__title { - padding: 6px 0; - } +.editor-mobile-navigation__icon { + cursor: pointer; + padding: 4px 12px; + color: lighten( $gray, 10% ); - .site__domain { - display: none; + &:hover { + color: $gray-dark; } - .site__title::after, - .site__domain::after { - @include long-content-fade( $color: $gray-light ); + &.is-selected { + cursor: default; + color: $gray-dark; } } -.editor-mobile-navigation__toggle { - margin-right: 8px; +.editor-mobile-navigation .editor-publish-button { + margin-right: 12px; } - diff --git a/client/post-editor/editor-publish-button/index.jsx b/client/post-editor/editor-publish-button/index.jsx new file mode 100644 index 000000000000..515fefc3795a --- /dev/null +++ b/client/post-editor/editor-publish-button/index.jsx @@ -0,0 +1,128 @@ +/** + * External dependencies + */ +import React, { PropTypes } from 'react'; + +/** + * Internal dependencies + */ +import localize from 'lib/mixins/i18n/localize'; +import stats from 'lib/posts/stats'; +import postUtils from 'lib/posts/utils'; +import siteUtils from 'lib/site/utils'; +import Button from 'components/button'; + +export const EditorPublishButton = React.createClass( { + propTypes: { + site: PropTypes.object, + post: PropTypes.object, + savedPost: PropTypes.object, + onSave: PropTypes.func, + onPublish: PropTypes.func, + tabIndex: PropTypes.number, + isPublishing: PropTypes.bool, + isSaveBlocked: PropTypes.bool, + hasContent: PropTypes.bool + }, + + trackClick: function() { + const postEvents = { + update: 'Clicked Update Post Button', + schedule: 'Clicked Schedule Post Button', + requestReview: 'Clicked Request-Review Post Button', + publish: 'Clicked Publish Post Button' + }; + const pageEvents = { + update: 'Clicked Update Page Button', + schedule: 'Clicked Schedule Page Button', + requestReview: 'Clicked Request-Review Page Button', + publish: 'Clicked Publish Page Button' + }; + const buttonState = this.getButtonState(); + const eventString = postUtils.isPage( this.props.post ) ? pageEvents[ buttonState ] : postEvents[ buttonState ]; + stats.recordEvent( eventString ); + stats.recordEvent( 'Clicked Primary Button' ); + }, + + getButtonState: function() { + if ( + postUtils.isPublished( this.props.savedPost ) && + ! postUtils.isBackDatedPublished( this.props.savedPost ) && + ! postUtils.isFutureDated( this.props.post ) || + ( + this.props.savedPost && + this.props.savedPost.status === 'future' && + postUtils.isFutureDated( this.props.post ) + ) + ) { + return 'update'; + } + + if ( postUtils.isFutureDated( this.props.post ) ) { + return 'schedule'; + } + + if ( siteUtils.userCan( 'publish_posts', this.props.site ) ) { + return 'publish'; + } + + if ( this.props.savedPost && this.props.savedPost.status === 'pending' ) { + return 'update'; + } + + return 'requestReview'; + }, + + getButtonLabel: function() { + switch ( this.getButtonState() ) { + case 'update': + return this.props.translate( 'Update' ); + case 'schedule': + return this.props.translate( 'Schedule' ); + case 'publish': + return this.props.translate( 'Publish' ); + case 'requestReview': + return this.props.translate( 'Submit for Review' ); + } + }, + + onClick: function() { + if ( postUtils.isFutureDated( this.props.post ) ) { + return this.props.onSave( 'future' ); + } + + if ( postUtils.isPublished( this.props.savedPost ) && + ! postUtils.isBackDatedPublished( this.props.savedPost ) + ) { + return this.props.onSave(); + } + + if ( siteUtils.userCan( 'publish_posts', this.props.site ) ) { + return this.props.onPublish(); + } + + return this.props.onSave( 'pending' ); + }, + + isEnabled: function() { + return ! this.props.isPublishing && + ! this.props.isSaveBlocked && + this.props.hasContent; + }, + + render: function() { + return ( + + ); + } +} ); + +export default localize( EditorPublishButton ); diff --git a/client/post-editor/editor-publish-button/test/index.jsx b/client/post-editor/editor-publish-button/test/index.jsx new file mode 100644 index 000000000000..fd17154964b1 --- /dev/null +++ b/client/post-editor/editor-publish-button/test/index.jsx @@ -0,0 +1,376 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; +import moment from 'moment'; +import React from 'react'; +import sinon from 'sinon'; +import TestUtils from 'react-addons-test-utils'; +import identity from 'lodash/identity'; + +/** + * Internal dependencies + */ +import useFakeDom from 'test/helpers/use-fake-dom'; + +/** + * Module variables + */ + +describe( 'EditorPublishButton', function() { + var EditorPublishButton, + shallow, + MOCK_SITE = { + capabilities: { + publish_posts: true + }, + options: {} + }; + + useFakeDom(); + + this.timeout( 10 * 1000 ); + + before( function() { + shallow = require( 'enzyme' ).shallow; + EditorPublishButton = require( '../' ).EditorPublishButton; + } ); + + describe( '#getButtonLabel()', function() { + it( 'should return Update if the post was originally published and is still slated to be published', function() { + const tree = shallow( + + ).instance(); + expect( tree.getButtonLabel() ).to.equal( 'Update' ); + } ); + + it( 'should return Update if the post was originally published and is currently reverted to non-published status', function() { + const tree = shallow( + + ).instance(); + + expect( tree.getButtonLabel() ).to.equal( 'Update' ); + } ); + + it( 'should return Schedule if the post is dated in the future and not scheduled', function() { + var now = moment( new Date() ), + nextMonth = now.month( now.month() + 1 ).format(), + tree; + + tree = shallow( + + ).instance(); + + expect( tree.getButtonLabel() ).to.equal( 'Schedule' ); + } ); + + it( 'should return Schedule if the post is dated in the future and published', function() { + var now = moment( new Date() ), + nextMonth = now.month( now.month() + 1 ).format(), + tree; + + tree = shallow( + + ).instance(); + + expect( tree.getButtonLabel() ).to.equal( 'Schedule' ); + } ); + + it( 'should return Update if the post is scheduled and dated in the future', function() { + var now = moment( new Date() ), + nextMonth = now.month( now.month() + 1 ).format(), + tree; + + tree = shallow( + + ).instance(); + + expect( tree.getButtonLabel() ).to.equal( 'Update' ); + } ); + + it( 'should return Update if the post is scheduled, dated in the future, and next status is draft', function() { + var now = moment( new Date() ), + nextMonth = now.month( now.month() + 1 ).format(), + tree; + + tree = shallow( + + ).instance(); + + expect( tree.getButtonLabel() ).to.equal( 'Update' ); + } ); + + it( 'should return Publish if the post is scheduled and dated in the past', function() { + var now = moment( new Date() ), + lastMonth = now.month( now.month() - 1 ).format(), + tree; + + tree = shallow( + + ).instance(); + + expect( tree.getButtonLabel() ).to.equal( 'Publish' ); + } ); + + it( 'should return Publish if the post is a draft', function() { + const tree = shallow( + + ).instance(); + + expect( tree.getButtonLabel() ).to.equal( 'Publish' ); + } ); + + it( 'should return "Submit for Review" if the post is a draft and user can\'t publish', function() { + const tree = shallow( + + ).instance(); + + expect( tree.getButtonLabel() ).to.equal( 'Submit for Review' ); + } ); + } ); + + describe( '#isEnabled()', function() { + it( 'should return true if form is not publishing and post is not empty', function() { + const tree = shallow( + ).instance(); + + expect( tree.isEnabled() ).to.be.true; + } ); + + it( 'should return true if form is not publishind and post is new and has content, but is not dirty', function() { + const tree = shallow( + ).instance(); + + expect( tree.isEnabled() ).to.be.true; + } ); + + it( 'should return false if form is publishing', function() { + const tree = shallow( + ).instance(); + + expect( tree.isEnabled() ).to.be.false; + } ); + + it( 'should return false if saving is blocked', function() { + const tree = shallow( + ).instance(); + + expect( tree.isEnabled() ).to.be.false; + } ); + + it( 'should return false if not dirty and has no content', function() { + const tree = shallow( + ).instance(); + + expect( tree.isEnabled() ).to.be.false; + } ); + + it( 'should return false if post has no content', function() { + const tree = shallow( + ).instance(); + + expect( tree.isEnabled() ).to.be.false; + } ); + } ); + + describe( '#onClick', function() { + it( 'should publish a draft', function() { + var onPublish = sinon.spy(), + tree; + + tree = shallow( + + ).instance(); + + tree.onClick(); + + expect( onPublish ).to.have.been.called; + } ); + + it( 'should schedule a posted dated in future', function() { + var now = moment( new Date() ), + nextMonth = now.month( now.month() + 1 ).format(), + onSave = sinon.spy(), + tree; + + tree = shallow( + + ).instance(); + + tree.onClick(); + + expect( onSave ).to.have.been.calledWith( 'future' ); + } ); + + it( 'should save a scheduled post dated in future', function() { + var now = moment( new Date() ), + nextMonth = now.month( now.month() + 1 ).format(), + onSave = sinon.spy(), + tree; + + tree = shallow( + + ).instance(); + + tree.onClick(); + + expect( onSave ).to.have.been.calledWith( 'future' ); + } ); + + it( 'should publish a scheduled post dated in past', function() { + var now = moment( new Date() ), + lastMonth = now.month( now.month() - 1 ).format(), + onPublish = sinon.spy(), + tree; + + tree = shallow( + + ).instance(); + + tree.onClick(); + + expect( onPublish ).to.have.been.called; + } ); + + it( 'should update a published post that has changed status', function() { + var onSave = sinon.spy(), + tree; + + tree = shallow( + + ).instance(); + + tree.onClick(); + + expect( onSave ).to.have.been.called; + } ); + + it( 'should set status to "pending" if the user can\'t publish', function() { + var onSave = sinon.spy(), + tree; + + tree = shallow( + + ).instance(); + + tree.onClick(); + + expect( onSave ).to.have.been.calledWith( 'pending' ); + } ); + } ); +} ); diff --git a/client/post-editor/editor-sidebar/header.jsx b/client/post-editor/editor-sidebar/header.jsx index de4e7a4ffeb6..61d6445f3bb9 100644 --- a/client/post-editor/editor-sidebar/header.jsx +++ b/client/post-editor/editor-sidebar/header.jsx @@ -20,7 +20,7 @@ import Button from 'components/button'; import Gridicon from 'components/gridicon'; import DraftsButton from 'post-editor/drafts-button'; -function EditorSidebarHeader( { translate, type, showDrafts, toggleDrafts, allPostsUrl, toggleSidebar } ) { +function EditorSidebarHeader( { translate, type, showDrafts, toggleDrafts, allPostsUrl } ) { const className = classnames( 'editor-sidebar__header', { 'is-drafts-visible': showDrafts } ); @@ -55,11 +55,6 @@ function EditorSidebarHeader( { translate, type, showDrafts, toggleDrafts, allPo { type === 'post' && ( ) } - ); } @@ -69,8 +64,7 @@ EditorSidebarHeader.propTypes = { type: PropTypes.string, showDrafts: PropTypes.bool, toggleDrafts: PropTypes.func, - allPostsUrl: PropTypes.string, - toggleSidebar: PropTypes.func + allPostsUrl: PropTypes.string }; export default connect( diff --git a/client/post-editor/editor-sidebar/index.jsx b/client/post-editor/editor-sidebar/index.jsx new file mode 100644 index 000000000000..a2566c48532a --- /dev/null +++ b/client/post-editor/editor-sidebar/index.jsx @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import React, { PropTypes } from 'react'; + +/** + * Internal dependencies + */ +import EditorDrawer from 'post-editor/editor-drawer'; +import EditorGroundControl from 'post-editor/editor-ground-control'; +import DraftList from 'my-sites/drafts/draft-list'; +import EditorSidebarHeader from './header'; + +export default React.createClass( { + displayName: 'EditorSidebar', + + propTypes: { + allPostsUrl: PropTypes.string, + sites: PropTypes.object, + onTitleClick: PropTypes.func, + savedPost: PropTypes.object, + post: PropTypes.object, + isNew: PropTypes.bool, + isDirty: PropTypes.bool, + isSaveBlocked: PropTypes.bool, + hasContent: PropTypes.bool, + isSaving: PropTypes.bool, + isPublishing: PropTypes.bool, + onSave: PropTypes.func, + onPreview: PropTypes.func, + onPublish: PropTypes.func, + onTrashingPost: PropTypes.func, + site: PropTypes.object, + type: PropTypes.string, + showDrafts: PropTypes.bool + }, + + render() { + return ( +
+ + { this.props.showDrafts + ? + :
+ + +
} +
+ ); + } + +} ); diff --git a/client/post-editor/editor-sidebar/style.scss b/client/post-editor/editor-sidebar/style.scss index cb7dc27dfe8a..473eb471135b 100644 --- a/client/post-editor/editor-sidebar/style.scss +++ b/client/post-editor/editor-sidebar/style.scss @@ -1,12 +1,27 @@ +.post-editor__sidebar { + @extend .sidebar; + + @include breakpoint( "<660px" ) { + position: relative; + top: 0; + left: 0; + display: none; + + .focus-sidebar & { + display: block; + transform: none; + } + } +} + .editor-sidebar__header { - display: flex; - flex-shrink: 0; - justify-content: flex-end; - align-items: center; - padding: 8px; + display: none; @include breakpoint( ">660px" ) { + display: flex; justify-content: space-between; + align-items: center; + padding: 8px; } } diff --git a/client/post-editor/editor-slug/index.jsx b/client/post-editor/editor-slug/index.jsx index 457ed30992f2..1005070ce1ba 100644 --- a/client/post-editor/editor-slug/index.jsx +++ b/client/post-editor/editor-slug/index.jsx @@ -36,7 +36,7 @@ const PostEditorSlug = React.createClass( { getDefaultProps() { return { - setSlug: () => {}, + setSlug: noop, onEscEnter: noop, isEditable: true }; diff --git a/client/post-editor/post-editor.jsx b/client/post-editor/post-editor.jsx index 967026716dc0..325a377463b0 100644 --- a/client/post-editor/post-editor.jsx +++ b/client/post-editor/post-editor.jsx @@ -18,9 +18,7 @@ const actions = require( 'lib/posts/actions' ), route = require( 'lib/route' ), PostEditStore = require( 'lib/posts/post-edit-store' ), EditorActionBar = require( 'post-editor/editor-action-bar' ), - EditorDrawer = require( 'post-editor/editor-drawer' ), FeaturedImage = require( 'post-editor/editor-featured-image' ), - EditorGroundControl = require( 'post-editor/editor-ground-control' ), EditorTitleContainer = require( 'post-editor/editor-title/container' ), EditorPageSlug = require( 'post-editor/editor-page-slug' ), NoticeAction = require( 'components/notice/notice-action' ), @@ -34,7 +32,6 @@ const actions = require( 'lib/posts/actions' ), layoutFocus = require( 'lib/layout-focus' ), titleActions = require( 'lib/screen-title/actions' ), observe = require( 'lib/mixins/data-observe' ), - DraftList = require( 'my-sites/drafts/draft-list' ), PreferencesActions = require( 'lib/preferences/actions' ), InvalidURLDialog = require( 'post-editor/invalid-url-dialog' ), RestorePostDialog = require( 'post-editor/restore-post-dialog' ), @@ -59,7 +56,7 @@ import { import { setEditorLastDraft, resetEditorLastDraft } from 'state/ui/editor/last-draft/actions'; import { isEditorDraftsVisible } from 'state/ui/editor/selectors'; import { toggleEditorDraftsVisible } from 'state/ui/editor/actions'; -import EditorSidebarHeader from 'post-editor/editor-sidebar/header'; +import EditorSidebar from 'post-editor/editor-sidebar'; const messages = { post: { @@ -327,8 +324,18 @@ const PostEditor = React.createClass( { return (
+
-
- { this.iframePreviewEnabled() - ? - : null } -
-
- - { this.props.showDrafts - ? - :
- - - -
}
+ + { this.iframePreviewEnabled() ? + + : null }
{ isTrashed ? 660px" ) { + display: block; backface-visibility: visible; perspective: none; } @@ -49,10 +61,6 @@ } } -.post-editor__sidebar { - @extend .sidebar; -} - .editor__header, .editor .mce-container-body, .editor .tinymce { @@ -136,14 +144,6 @@ width: 160px; z-index: z-index( 'root', '.editor__switch-mode' ); - .focus-sidebar & { - z-index: auto; - - @include breakpoint( ">660px" ) { - z-index: z-index( 'root', '.editor__switch-mode' ); - } - } - @include breakpoint( ">960px" ) { right: 32px; } diff --git a/client/post-editor/test/post-editor.jsx b/client/post-editor/test/post-editor.jsx index c601f6853b4d..7dc702d08aff 100644 --- a/client/post-editor/test/post-editor.jsx +++ b/client/post-editor/test/post-editor.jsx @@ -47,13 +47,14 @@ describe( 'PostEditor', function() { mockery.registerMock( 'post-editor/editor-title/container', MOCK_COMPONENT ); mockery.registerMock( 'post-editor/editor-page-slug', MOCK_COMPONENT ); mockery.registerMock( 'post-editor/editor-media-advanced', MOCK_COMPONENT ); + mockery.registerMock( 'post-editor/editor-mobile-navigation', MOCK_COMPONENT ); mockery.registerMock( 'post-editor/editor-author', MOCK_COMPONENT ); mockery.registerMock( 'post-editor/editor-visibility', MOCK_COMPONENT ); mockery.registerMock( 'post-editor/editor-word-count', MOCK_COMPONENT ); mockery.registerMock( 'post-editor/editor-preview', MOCK_COMPONENT ); mockery.registerMock( 'post-editor/invalid-url-dialog', MOCK_COMPONENT ); mockery.registerMock( 'post-editor/restore-post-dialog', MOCK_COMPONENT ); - mockery.registerMock( 'post-editor/editor-sidebar/header', MOCK_COMPONENT ); + mockery.registerMock( 'post-editor/editor-sidebar', MOCK_COMPONENT ); mockery.registerMock( './editor-preview', MOCK_COMPONENT ); mockery.registerMock( 'my-sites/drafts/draft-list', MOCK_COMPONENT ); mockery.registerMock( 'lib/layout-focus', { set() {} } );