diff --git a/components/index.js b/components/index.js
index e29243f1bff079..8d823b582bd77a 100644
--- a/components/index.js
+++ b/components/index.js
@@ -13,6 +13,7 @@ export { default as NoticeList } from './notice/list';
export { default as Panel } from './panel';
export { default as PanelHeader } from './panel/header';
export { default as PanelBody } from './panel/body';
+export { default as PanelRow } from './panel/row';
export { default as Placeholder } from './placeholder';
export { default as ResponsiveWrapper } from './responsive-wrapper';
export { default as SandBox } from './sandbox';
diff --git a/components/panel/row.js b/components/panel/row.js
new file mode 100644
index 00000000000000..ca9e9e4854dbf8
--- /dev/null
+++ b/components/panel/row.js
@@ -0,0 +1,16 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+function PanelRow( { className, children } ) {
+ const classes = classnames( 'components-panel__row', className );
+
+ return (
+
+ { children }
+
+ );
+}
+
+export default PanelRow;
diff --git a/components/panel/style.scss b/components/panel/style.scss
index 0d0dbcfb6562e0..058faed1d7eb99 100644
--- a/components/panel/style.scss
+++ b/components/panel/style.scss
@@ -77,3 +77,14 @@
.components-panel__body-toggle-icon {
margin-right: -5px;
}
+
+.components-panel__row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 20px;
+
+ &:empty {
+ margin-top: 0;
+ }
+}
diff --git a/editor/sidebar/discussion-panel/index.js b/editor/sidebar/discussion-panel/index.js
index 1801e931311ac2..97bb1972296ada 100644
--- a/editor/sidebar/discussion-panel/index.js
+++ b/editor/sidebar/discussion-panel/index.js
@@ -7,12 +7,11 @@ import { connect } from 'react-redux';
* WordPress dependencies
*/
import { __ } from 'i18n';
-import { PanelBody, FormToggle, withInstanceId } from 'components';
+import { PanelBody, PanelRow, FormToggle, withInstanceId } from 'components';
/**
* Internal Dependencies
*/
-import './style.scss';
import { getEditedPostAttribute } from '../../selectors';
import { editPost } from '../../actions';
@@ -25,7 +24,7 @@ function DiscussionPanel( { pingStatus = 'open', commentStatus = 'open', instanc
return (
-
+
-
-
+
+
-
+
);
}
diff --git a/editor/sidebar/discussion-panel/style.scss b/editor/sidebar/discussion-panel/style.scss
deleted file mode 100644
index 1e9219bed17fe7..00000000000000
--- a/editor/sidebar/discussion-panel/style.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.editor-discussion-panel__row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 20px;
-}
diff --git a/editor/sidebar/page-attributes/index.js b/editor/sidebar/page-attributes/index.js
new file mode 100644
index 00000000000000..74b922f5cf41cd
--- /dev/null
+++ b/editor/sidebar/page-attributes/index.js
@@ -0,0 +1,109 @@
+/**
+ * External dependencies
+ */
+import { connect } from 'react-redux';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from 'i18n';
+import { Component } from 'element';
+import { PanelBody, PanelRow, withInstanceId } from 'components';
+
+/**
+ * Internal dependencies
+ */
+import { editPost } from '../../actions';
+import { getCurrentPostType, getEditedPostAttribute } from '../../selectors';
+
+export class PageAttributes extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.setUpdatedOrder = this.setUpdatedOrder.bind( this );
+
+ this.state = {
+ supportsPageAttributes: false,
+ };
+ }
+
+ componentDidMount() {
+ this.fetchSupports();
+ }
+
+ fetchSupports() {
+ const { postTypeSlug } = this.props;
+ this.fetchSupportsRequest = new wp.api.models.Type( { id: postTypeSlug } )
+ .fetch( { data: { context: 'edit' } } )
+ .done( ( postType ) => {
+ const {
+ 'page-attributes': supportsPageAttributes = false,
+ } = postType.supports;
+
+ this.setState( { supportsPageAttributes } );
+ } );
+ }
+
+ componentWillUnmount() {
+ if ( this.fetchSupportsRequest ) {
+ this.fetchSupportsRequest.abort();
+ }
+ }
+
+ setUpdatedOrder( event ) {
+ const order = Number( event.target.value );
+ if ( order >= 0 ) {
+ this.props.onUpdateOrder( order );
+ }
+ }
+
+ render() {
+ const { instanceId, order } = this.props;
+ const { supportsPageAttributes } = this.state;
+
+ // Only render fields if post type supports page attributes
+ if ( ! supportsPageAttributes ) {
+ return null;
+ }
+
+ // Create unique identifier for inputs
+ const inputId = `editor-page-attributes__order-${ instanceId }`;
+
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+
+export default connect(
+ ( state ) => {
+ return {
+ postTypeSlug: getCurrentPostType( state ),
+ order: getEditedPostAttribute( state, 'menu_order' ),
+ };
+ },
+ ( dispatch ) => {
+ return {
+ onUpdateOrder( order ) {
+ dispatch( editPost( {
+ menu_order: order,
+ } ) );
+ },
+ };
+ }
+)( withInstanceId( PageAttributes ) );
diff --git a/editor/sidebar/page-attributes/test/index.js b/editor/sidebar/page-attributes/test/index.js
new file mode 100644
index 00000000000000..c87d6c6435009c
--- /dev/null
+++ b/editor/sidebar/page-attributes/test/index.js
@@ -0,0 +1,68 @@
+/**
+ * External dependencies
+ */
+import { shallow } from 'enzyme';
+
+/**
+ * Internal dependencies
+ */
+import { PageAttributes } from '../';
+
+describe( 'PageAttributes', () => {
+ let fetchSupports;
+ beforeEach( () => {
+ fetchSupports = PageAttributes.prototype.fetchSupports;
+ PageAttributes.prototype.fetchSupports = jest.fn();
+ } );
+
+ afterEach( () => {
+ PageAttributes.prototype.fetchSupports = fetchSupports;
+ } );
+
+ it( 'should not render anything if no page attributes support', () => {
+ const wrapper = shallow( );
+
+ expect( wrapper.type() ).toBe( null );
+ } );
+
+ it( 'should render input if page attributes support', () => {
+ const wrapper = shallow( );
+ wrapper.setState( { supportsPageAttributes: true } );
+
+ expect( wrapper.type() ).not.toBe( null );
+ } );
+
+ it( 'should reject invalid input', () => {
+ const onUpdateOrder = jest.fn();
+ const wrapper = shallow( );
+ wrapper.setState( { supportsPageAttributes: true } );
+
+ wrapper.find( 'input' ).simulate( 'change', {
+ target: {
+ value: -1,
+ },
+ } );
+
+ wrapper.find( 'input' ).simulate( 'change', {
+ target: {
+ value: 'bad',
+ },
+ } );
+
+ expect( onUpdateOrder ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should update with valid input', () => {
+ const onUpdateOrder = jest.fn();
+ const wrapper = shallow( );
+ wrapper.setState( { supportsPageAttributes: true } );
+
+ wrapper.find( 'input' ).simulate( 'change', {
+ target: {
+ value: 4,
+ },
+ } );
+
+ expect( onUpdateOrder ).toHaveBeenCalledWith( 4 );
+ } );
+} );
diff --git a/editor/sidebar/post-schedule/index.js b/editor/sidebar/post-schedule/index.js
index 5e2e72c87dbb51..de6758d1a89244 100644
--- a/editor/sidebar/post-schedule/index.js
+++ b/editor/sidebar/post-schedule/index.js
@@ -12,6 +12,7 @@ import moment from 'moment';
import { __ } from 'i18n';
import { Component } from 'element';
import { dateI18n, settings } from 'date';
+import { PanelRow } from 'components';
/**
* Internal dependencies
@@ -58,7 +59,7 @@ class PostSchedule extends Component {
);
return (
-
}
-
+
);
}
}
diff --git a/editor/sidebar/post-schedule/style.scss b/editor/sidebar/post-schedule/style.scss
index d3ad68dde96e53..e179ad4d32eebf 100644
--- a/editor/sidebar/post-schedule/style.scss
+++ b/editor/sidebar/post-schedule/style.scss
@@ -1,9 +1,6 @@
@import '~react-datepicker/dist/react-datepicker';
.editor-post-schedule {
- display: flex;
- justify-content: space-between;
- align-items: center;
width: 100%;
position: relative;
}
diff --git a/editor/sidebar/post-settings/index.js b/editor/sidebar/post-settings/index.js
index 6621b621d5a61f..7a78cc1f8dc132 100644
--- a/editor/sidebar/post-settings/index.js
+++ b/editor/sidebar/post-settings/index.js
@@ -1,8 +1,3 @@
-/**
- * External dependencies
- */
-import { connect } from 'react-redux';
-
/**
* WordPress dependencies
*/
@@ -19,24 +14,19 @@ import FeaturedImage from '../featured-image';
import DiscussionPanel from '../discussion-panel';
import LastRevision from '../last-revision';
import TableOfContents from '../table-of-contents';
+import PageAttributes from '../page-attributes';
-const PostSettings = () => {
- return (
-
-
-
-
-
-
-
-
-
- );
-};
+const panel = (
+
+
+
+
+
+
+
+
+
+
+);
-export default connect(
- undefined,
- ( dispatch ) => ( {
- toggleSidebar: () => dispatch( { type: 'TOGGLE_SIDEBAR' } ),
- } )
-)( PostSettings );
+export default () => panel;
diff --git a/editor/sidebar/post-status/index.js b/editor/sidebar/post-status/index.js
index f367e463646d23..d91ba764a57199 100644
--- a/editor/sidebar/post-status/index.js
+++ b/editor/sidebar/post-status/index.js
@@ -8,12 +8,11 @@ import { connect } from 'react-redux';
*/
import { __ } from 'i18n';
import { Component } from 'element';
-import { PanelBody, FormToggle, withInstanceId } from 'components';
+import { PanelBody, PanelRow, FormToggle, withInstanceId } from 'components';
/**
* Internal Dependencies
*/
-import './style.scss';
import PostVisibility from '../post-visibility';
import PostTrash from '../post-trash';
import PostSchedule from '../post-schedule';
@@ -48,7 +47,7 @@ class PostStatus extends Component {
return (
{ ! isPublished &&
-
+
-
+
}
-
-
-
+
+
+
{ __( 'Post Format' ) }
{ format }
-
+
-
+
);
}
diff --git a/editor/sidebar/post-status/style.scss b/editor/sidebar/post-status/style.scss
deleted file mode 100644
index 82e085a5b71a8a..00000000000000
--- a/editor/sidebar/post-status/style.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-.editor-post-status__row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 20px;
-
- &:empty {
- padding-top: 0;
- }
-}
diff --git a/editor/sidebar/post-sticky/index.js b/editor/sidebar/post-sticky/index.js
index 5909d6ec3f6c68..965e70828af605 100644
--- a/editor/sidebar/post-sticky/index.js
+++ b/editor/sidebar/post-sticky/index.js
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
* WordPress dependencies
*/
import { __ } from 'i18n';
-import { FormToggle, withInstanceId } from 'components';
+import { PanelRow, FormToggle, withInstanceId } from 'components';
/**
* Internal dependencies
@@ -23,7 +23,7 @@ function PostSticky( { onUpdateSticky, postType, postSticky = false, instanceId
const stickyToggleId = 'post-sticky-toggle-' + instanceId;
return (
-
+
-
+
);
}
diff --git a/editor/sidebar/post-trash/index.js b/editor/sidebar/post-trash/index.js
index ce0ebbd4e3ae23..6e64b153c564ac 100644
--- a/editor/sidebar/post-trash/index.js
+++ b/editor/sidebar/post-trash/index.js
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
* WordPress dependencies
*/
import { __ } from 'i18n';
-import { Button, Dashicon } from 'components';
+import { PanelRow, Button, Dashicon } from 'components';
/**
* Internal dependencies
@@ -28,10 +28,12 @@ function PostTrash( { isNew, postId, postType, ...props } ) {
const onClick = () => props.trashPost( postId, postType );
return (
-
+
+
+
);
}
diff --git a/editor/sidebar/post-visibility/index.js b/editor/sidebar/post-visibility/index.js
index 8f663a07bd2bf2..eb857cbefa0783 100644
--- a/editor/sidebar/post-visibility/index.js
+++ b/editor/sidebar/post-visibility/index.js
@@ -4,13 +4,13 @@
import { connect } from 'react-redux';
import clickOutside from 'react-click-outside';
import { find } from 'lodash';
-import { withInstanceId } from 'components';
/**
* WordPress dependencies
*/
import { __ } from 'i18n';
import { Component } from 'element';
+import { PanelRow, withInstanceId } from 'components';
/**
* Internal Dependencies
@@ -104,7 +104,7 @@ class PostVisibility extends Component {
// Disable Reason: The input is inside the label, we shouldn't need the htmlFor
/* eslint-disable jsx-a11y/label-has-for */
return (
-
+
{ __( 'Visibility' ) }
+
);
/* eslint-enable jsx-a11y/label-has-for */
}
diff --git a/editor/sidebar/post-visibility/style.scss b/editor/sidebar/post-visibility/style.scss
index c48ffa99015eae..06c86e1fc3469b 100644
--- a/editor/sidebar/post-visibility/style.scss
+++ b/editor/sidebar/post-visibility/style.scss
@@ -1,7 +1,4 @@
.editor-post-visibility {
- display: flex;
- justify-content: space-between;
- align-items: center;
width: 100%;
position: relative;
}