From 20d8baf9b995fac2875dda76498fafd4a75989a5 Mon Sep 17 00:00:00 2001 From: John Godley Date: Wed, 25 Nov 2015 11:57:19 +0000 Subject: [PATCH 01/89] Open window with previewUrl if it failed In the desktop app `window.open( 'about:blank' )` fails. Add a check for the preview window. If it doesn't exist when we have the previewUrl then directly open the window with the preview URL. It may be possible to just create the window here in the first place but I am not sure if it is split for a reason --- client/post-editor/post-editor.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/post-editor/post-editor.jsx b/client/post-editor/post-editor.jsx index c126fb0ce9b9c..726042b16680f 100644 --- a/client/post-editor/post-editor.jsx +++ b/client/post-editor/post-editor.jsx @@ -628,8 +628,12 @@ var PostEditor = React.createClass( { } previewPost = function() { - this._previewWindow.location = this.state.previewUrl; - this._previewWindow.focus(); + if ( this._previewWindow ) { + this._previewWindow.location = this.state.previewUrl; + this._previewWindow.focus(); + } else { + this._previewWindow = window.open( this.state.previewUrl, 'WordPress.com Post Preview' ); + } }.bind( this ); if ( status === 'publish' ) { From ae6a26127eba800a05744cc2462111d4709daa45 Mon Sep 17 00:00:00 2001 From: artpi Date: Thu, 3 Dec 2015 11:59:13 +0100 Subject: [PATCH 02/89] SiteSettings: Add stopPropagation to SELECT onClick events onClick was propagating to label, triggering click events higher in DOM tree in Safari. Fixes #86 --- client/my-sites/site-settings/form-base.js | 5 +++++ client/my-sites/site-settings/form-discussion.jsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/client/my-sites/site-settings/form-base.js b/client/my-sites/site-settings/form-base.js index bfac43a5417b4..eb105ee7fd36b 100644 --- a/client/my-sites/site-settings/form-base.js +++ b/client/my-sites/site-settings/form-base.js @@ -62,6 +62,11 @@ module.exports = { }, + recordClickEventAndStop: function( recordObject, clickEvent ) { + this.recordEvent( recordObject ); + clickEvent.preventDefault(); + }, + recordEvent: function( eventAction ) { analytics.ga.recordEvent( 'Site Settings', eventAction ); }, diff --git a/client/my-sites/site-settings/form-discussion.jsx b/client/my-sites/site-settings/form-discussion.jsx index 7f69ba34b21f4..b238b94faf34d 100644 --- a/client/my-sites/site-settings/form-discussion.jsx +++ b/client/my-sites/site-settings/form-discussion.jsx @@ -178,7 +178,7 @@ module.exports = React.createClass( { name="thread_comments_depth" valueLink={ this.linkState( 'thread_comments_depth' ) } disabled={ this.state.fetchingSettings } - onClick={ this.recordEvent.bind( this, 'Selected Comment Nesting Level' ) } + onClick={ this.recordClickEventAndStop.bind( this, 'Selected Comment Nesting Level' ) } > @@ -223,7 +223,7 @@ module.exports = React.createClass( { name="default_comments_page" valueLink={ this.linkState( 'default_comments_page' ) } disabled={ this.state.fetchingSettings } - onClick={ this.recordEvent.bind( this, 'Selected Comment Page Display Default' ) } + onClick={ this.recordClickEventAndStop.bind( this, 'Selected Comment Page Display Default' ) } > From 09b441db491526a80f44af58a79a3ab964f8c287 Mon Sep 17 00:00:00 2001 From: John Godley Date: Wed, 25 Nov 2015 13:55:08 +0000 Subject: [PATCH 03/89] Add a desktop print function --- client/lib/desktop/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/lib/desktop/index.js b/client/lib/desktop/index.js index 9c29146326ae5..6c261c79220be 100644 --- a/client/lib/desktop/index.js +++ b/client/lib/desktop/index.js @@ -141,6 +141,10 @@ var Desktop = { onCookieAuthComplete: function() { var iframe = document.querySelector( '#wpnt-notes-iframe2' ); iframe.src = iframe.src; + }, + + print: function( title, html ) { + ipc.send( 'print', title, html ); } }; From fff6a42071ffdd424cb3f092db07deb4f7669b8f Mon Sep 17 00:00:00 2001 From: John Godley Date: Wed, 25 Nov 2015 13:55:20 +0000 Subject: [PATCH 04/89] On the desktop send backup codes to Electron --- .../security-2fa-backup-codes-list/index.jsx | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/client/me/security-2fa-backup-codes-list/index.jsx b/client/me/security-2fa-backup-codes-list/index.jsx index 02e9e3a1bd5d9..8df7c0434bdb2 100644 --- a/client/me/security-2fa-backup-codes-list/index.jsx +++ b/client/me/security-2fa-backup-codes-list/index.jsx @@ -12,6 +12,7 @@ var FormButton = require( 'components/forms/form-button' ), FormButtonBar = require( 'components/forms/form-buttons-bar' ), FormCheckbox = require( 'components/forms/form-checkbox' ), FormLabel = require( 'components/forms/form-label' ), + config = require( 'config' ), Notice = require( 'components/notice' ); module.exports = React.createClass( { @@ -62,29 +63,35 @@ module.exports = React.createClass( { onPrint: function( event ) { event.preventDefault(); - if ( this.openPopup() ) { + + if ( config.isEnabled( 'desktop' ) ) { + require( 'lib/desktop' ).print( this.translate( 'Backup verification codes' ), this.getBackupCodeHTML( this.props.backupCodes ) ); + } else if ( this.openPopup() ) { this.doPopup( this.props.backupCodes ); } }, - doPopup: function( codes ) { - var datePrinted = this.moment().format( 'MMM DD, YYYY @ h:mm a' ); - var row; + getBackupCodeHTML: function( codes ) { + const datePrinted = this.moment().format( 'MMM DD, YYYY @ h:mm a' ); + let row; + let html = ''; - this.popup.document.open( 'text/html' ); - this.popup.document.write( '<html><body style="font-family:sans-serif">' ); - this.popup.document.write( '<div style="padding:10px; border:1px dashed black; display:inline-block">' ); - this.popup.document.write( + html += this.translate( 'Backup verification codes' ); + html += ''; + html += ''; + + html += '
'; + html += ( '

' + this.translate( 'Backup verification codes' ) + '

' ); - this.popup.document.write( '' ); - this.popup.document.write( '' ); + html += '
'; + html += ''; for ( row = 0; row < 5; row++ ) { - this.popup.document.write( + html += ( '' + '
' + ( row + 1 ) + '. ' + '' + codes[ row * 2 ] + '' + @@ -96,9 +103,9 @@ module.exports = React.createClass( { ); } - this.popup.document.write( '
' ); + html += ''; - this.popup.document.write( + html += ( '

' + this.translate( 'Printed: %(datePrinted)s', @@ -111,8 +118,13 @@ module.exports = React.createClass( { '

' ); - this.popup.document.write( '
' ); + html += ''; + return html; + }, + doPopup: function( codes ) { + this.popup.document.open( 'text/html' ); + this.popup.document.write( this.getBackupCodeHTML( codes ) ); this.popup.document.close(); this.popup.print(); From 6a4462f196b5b840f77cce4ae9a7a7e6e266df32 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Mon, 7 Dec 2015 16:05:05 +0100 Subject: [PATCH 05/89] Plugin-browser-list: refactored to ES6 --- .../plugins/plugins-browser-list/index.jsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/my-sites/plugins/plugins-browser-list/index.jsx b/client/my-sites/plugins/plugins-browser-list/index.jsx index e1b85b9967291..7e73991d2927e 100644 --- a/client/my-sites/plugins/plugins-browser-list/index.jsx +++ b/client/my-sites/plugins/plugins-browser-list/index.jsx @@ -1,27 +1,27 @@ /** * External dependencies */ -var React = require( 'react' ); +import React from 'react' /** * Internal dependencies */ -var PluginBrowserItem = require( 'my-sites/plugins/plugins-browser-item' ), - Gridicon = require( 'components/gridicon' ); +import PluginBrowserItem from 'my-sites/plugins/plugins-browser-item' +import Gridicon from 'components/gridicon' +import SectionHeader from 'components/section-header' -module.exports = React.createClass( { +export default React.createClass( { displayName: 'PluginsBrowserList', _DEFAULT_PLACEHOLDER_NUMBER: 6, - getPluginsViewList: function() { - var pluginsViewsList, - emptyCounter = 0; + getPluginsViewList() { + let emptyCounter = 0; - pluginsViewsList = this.props.plugins.map( function( plugin, n ) { + let pluginsViewsList = this.props.plugins.map( ( plugin, n ) => { return ; - }, this ); + } ); if ( this.props.showPlaceholders ) { pluginsViewsList = pluginsViewsList.concat( this.getPlaceholdersViews() ); @@ -39,13 +39,13 @@ module.exports = React.createClass( { return pluginsViewsList; }, - getPlaceholdersViews: function() { - return Array.apply( null, Array( this.props.size || this._DEFAULT_PLACEHOLDER_NUMBER ) ).map( function( item, i ) { + getPlaceholdersViews() { + return Array.apply( null, Array( this.props.size || this._DEFAULT_PLACEHOLDER_NUMBER ) ).map( ( item, i ) => { return ; } ); }, - getViews: function() { + getViews() { if ( this.props.plugins.length ) { return this.getPluginsViewList(); } else if ( this.props.showPlaceholders ) { @@ -53,7 +53,7 @@ module.exports = React.createClass( { } }, - getLink: function() { + getLink() { if ( this.props.expandedListLink ) { return { this.translate( 'See All' ) } @@ -62,7 +62,7 @@ module.exports = React.createClass( { } }, - render: function() { + render() { return (
From 99d6d57c8c6f3582a7a3350140935522edae30d8 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Mon, 7 Dec 2015 16:08:51 +0100 Subject: [PATCH 06/89] Plugins-Browser-list: Apply section header --- client/my-sites/plugins/plugins-browser-list/index.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/client/my-sites/plugins/plugins-browser-list/index.jsx b/client/my-sites/plugins/plugins-browser-list/index.jsx index 7e73991d2927e..5b435e48dc51d 100644 --- a/client/my-sites/plugins/plugins-browser-list/index.jsx +++ b/client/my-sites/plugins/plugins-browser-list/index.jsx @@ -9,6 +9,7 @@ import React from 'react' import PluginBrowserItem from 'my-sites/plugins/plugins-browser-item' import Gridicon from 'components/gridicon' import SectionHeader from 'components/section-header' +import SectionHeaderButton from 'components/section-header/button' export default React.createClass( { @@ -65,12 +66,9 @@ export default React.createClass( { render() { return (
-
-

- { this.props.title } -

+ { this.getLink() } -
+
{ this.getViews() }
From abe9eb88c2c1674f4f53285256cd355627dbe918 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Mon, 7 Dec 2015 16:50:03 +0100 Subject: [PATCH 07/89] Plugins-browser: Use Card for plugin browser lists --- client/my-sites/plugins/plugins-browser-list/index.jsx | 6 +++--- client/my-sites/plugins/plugins-browser-list/style.scss | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/my-sites/plugins/plugins-browser-list/index.jsx b/client/my-sites/plugins/plugins-browser-list/index.jsx index 5b435e48dc51d..39797d3bcac4a 100644 --- a/client/my-sites/plugins/plugins-browser-list/index.jsx +++ b/client/my-sites/plugins/plugins-browser-list/index.jsx @@ -7,9 +7,9 @@ import React from 'react' * Internal dependencies */ import PluginBrowserItem from 'my-sites/plugins/plugins-browser-item' +import Card from 'components/card' import Gridicon from 'components/gridicon' import SectionHeader from 'components/section-header' -import SectionHeaderButton from 'components/section-header/button' export default React.createClass( { @@ -69,9 +69,9 @@ export default React.createClass( { { this.getLink() } -
+ { this.getViews() } -
+
); } diff --git a/client/my-sites/plugins/plugins-browser-list/style.scss b/client/my-sites/plugins/plugins-browser-list/style.scss index b1dfd3b4f908c..7e62ac0f879ce 100644 --- a/client/my-sites/plugins/plugins-browser-list/style.scss +++ b/client/my-sites/plugins/plugins-browser-list/style.scss @@ -44,4 +44,5 @@ .plugins-browser-list__elements { min-height: 50px; overflow: hidden; // lazy clearfix + padding: 0; } From 2957c665fce92f938b413516745fec33be2e08dd Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Mon, 7 Dec 2015 17:27:34 +0100 Subject: [PATCH 08/89] Plugins-browser: Fix 'see all' button padding --- client/my-sites/plugins/plugins-browser-list/style.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/my-sites/plugins/plugins-browser-list/style.scss b/client/my-sites/plugins/plugins-browser-list/style.scss index 7e62ac0f879ce..0215ff9b5155d 100644 --- a/client/my-sites/plugins/plugins-browser-list/style.scss +++ b/client/my-sites/plugins/plugins-browser-list/style.scss @@ -17,7 +17,7 @@ .button.plugins-browser-list__select-all, .plugins-browser-list__title { display: inline-block; - padding: 6px 16px 7px; + padding: 6px 0px 7px; color: $gray; font-size: 11px; line-height: 1.6; @@ -38,7 +38,6 @@ .button.plugins-browser-list__select-all { float: right; - padding-right: 16px; } .plugins-browser-list__elements { From 67924f42eff5df8a4bd914c1e588457c5bd3625a Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Tue, 24 Nov 2015 16:43:15 -0800 Subject: [PATCH 09/89] PostListStore: Add factory for multiple store support and basic tests. --- client/components/post-list-fetcher/index.jsx | 50 +- client/lib/posts/actions.js | 53 +- client/lib/posts/post-counts-store.js | 2 +- client/lib/posts/post-list-cache-store.js | 2 +- client/lib/posts/post-list-store-factory.js | 24 + client/lib/posts/post-list-store.js | 603 +++++++++--------- client/lib/posts/test/post-list-store.js | 118 ++++ .../my-sites/stats/post-performance/index.jsx | 2 +- 8 files changed, 512 insertions(+), 342 deletions(-) create mode 100644 client/lib/posts/post-list-store-factory.js create mode 100644 client/lib/posts/test/post-list-store.js diff --git a/client/components/post-list-fetcher/index.jsx b/client/components/post-list-fetcher/index.jsx index d24a468387c1b..ba3fd99eaef51 100644 --- a/client/components/post-list-fetcher/index.jsx +++ b/client/components/post-list-fetcher/index.jsx @@ -6,7 +6,7 @@ var React = require( 'react' ); /** * Internal dependencies */ -var PostListStore = require( 'lib/posts/post-list-store' ), +var postListStoreFactory = require( 'lib/posts/post-list-store-factory' ), PostContentImagesStore = require( 'lib/posts/post-content-images-store' ), Dispatcher = require( 'dispatcher' ), actions = require( 'lib/posts/actions' ), @@ -14,11 +14,12 @@ var PostListStore = require( 'lib/posts/post-list-store' ), var PostListFetcher; -function dispatchQueryActions( query ) { - actions.queryPosts( query ); +function dispatchQueryActions( postListStoreId, query ) { + var postListStore = postListStoreFactory( postListStoreId ); + actions.queryPosts( query, postListStoreId ); - if ( PostListStore.getPage() === 0 ) { - actions.fetchNextPage(); + if ( postListStore.getPage() === 0 ) { + actions.fetchNextPage( postListStoreId ); } } @@ -45,22 +46,23 @@ function queryPosts( props ) { // Not ideal nor a best practice if ( Dispatcher.isDispatching() ) { setTimeout( function() { - dispatchQueryActions( query ); + dispatchQueryActions( props.postListStoreId, query ); }, 0 ); } else { - dispatchQueryActions( query ); + dispatchQueryActions( props.postListStoreId, query ); } } -function getPostsState() { +function getPostsState( postListStoreId ) { + var postListStore = postListStoreFactory( postListStoreId ); return { - listId: PostListStore.getID(), - posts: PostListStore.getAll(), + listId: postListStore.getID(), + posts: postListStore.getAll(), postImages: PostContentImagesStore.getAll(), - page: PostListStore.getPage(), - lastPage: PostListStore.isLastPage(), - loading: PostListStore.isFetchingNextPage(), - hasRecentError: PostListStore.hasRecentError() + page: postListStore.getPage(), + lastPage: postListStore.isLastPage(), + loading: postListStore.isFetchingNextPage(), + hasRecentError: postListStore.hasRecentError() }; } @@ -79,7 +81,8 @@ function shouldQueryPosts( props, nextProps ) { props.number !== nextProps.number || props.before !== nextProps.before || props.after !== nextProps.after || - props.siteID !== nextProps.siteID; + props.siteID !== nextProps.siteID || + props.postListStoreId !== nextProps.postListStoreId; } PostListFetcher = React.createClass( { @@ -100,18 +103,21 @@ PostListFetcher = React.createClass( { order: React.PropTypes.oneOf( [ 'ASC', 'DESC' ] ), number: React.PropTypes.number, before: React.PropTypes.string, - after: React.PropTypes.string + after: React.PropTypes.string, + postListStoreId: React.PropTypes.string }, getDefaultProps: function() { return { orderBy: 'date', - order: 'DESC' + order: 'DESC', + postListStoreId: 'default' }; }, componentWillMount: function() { - PostListStore.on( 'change', this.onPostsChange ); + var postListStore = postListStoreFactory( this.props.postListStoreId ); + postListStore.on( 'change', this.onPostsChange ); if ( this.props.withImages ) { PostContentImagesStore.on( 'change', this.onPostsChange ); } @@ -119,12 +125,14 @@ PostListFetcher = React.createClass( { }, componentDidMount: function() { - this._poller = pollers.add( PostListStore, actions.fetchUpdated, { interval: 60000, leading: false } ); + var postListStore = postListStoreFactory( this.props.postListStoreId ); + this._poller = pollers.add( postListStore, actions.fetchUpdated, { interval: 60000, leading: false } ); }, componentWillUnmount: function() { + var postListStore = postListStoreFactory( this.props.postListStoreId ); pollers.remove( this._poller ); - PostListStore.off( 'change', this.onPostsChange ); + postListStore.off( 'change', this.onPostsChange ); if ( this.props.withImages ) { PostContentImagesStore.off( 'change', this.onPostsChange ); } @@ -144,7 +152,7 @@ PostListFetcher = React.createClass( { }, onPostsChange: function() { - this.setState( getPostsState( this.props ) ); + this.setState( getPostsState( this.props.postListStoreId ) ); }, render: function() { diff --git a/client/lib/posts/actions.js b/client/lib/posts/actions.js index 3db00e0a4f724..3e02c9744bb95 100644 --- a/client/lib/posts/actions.js +++ b/client/lib/posts/actions.js @@ -14,7 +14,7 @@ var debug = require( 'debug' )( 'calypso:posts' ), var wpcom = require( 'lib/wp' ), PostsStore = require( './posts-store' ), PostEditStore = require( './post-edit-store' ), - PostListStore = require( './post-list-store' ), + postListStoreFactory = require( './post-list-store-factory' ), PreferencesStore = require( 'lib/preferences/store' ), sites = require( 'lib/sites-list' )(), utils = require( './utils' ), @@ -423,10 +423,11 @@ PostActions = { postHandle.restore( PostActions.receiveUpdate.bind( null, callback ) ); }, - queryPosts: function( options ) { + queryPosts: function( options, postListStoreId = 'default' ) { Dispatcher.handleViewAction( { type: 'QUERY_POSTS', - options: options + options: options, + postListStoreId: postListStoreId } ); }, @@ -435,69 +436,77 @@ PostActions = { * * @api public */ - fetchNextPage: function() { - var params, id, siteID; + fetchNextPage: function( postListStoreId = 'default' ) { + var params, id, siteID, postListStore; - if ( PostListStore.isLastPage() ) { + postListStore = postListStoreFactory( postListStoreId ); + + if ( postListStore.isLastPage() ) { return; } Dispatcher.handleViewAction( { - type: 'FETCH_NEXT_POSTS_PAGE' + type: 'FETCH_NEXT_POSTS_PAGE', + postListStoreId: postListStoreId } ); - id = PostListStore.getID(); - params = PostListStore.getNextPageParams(); - siteID = PostListStore.getSiteID(); + id = postListStore.getID(); + params = postListStore.getNextPageParams(); + siteID = postListStore.getSiteID(); if ( siteID ) { wpcom .site( siteID ) - .postsList( params, PostActions.receivePage.bind( null, id ) ); + .postsList( params, PostActions.receivePage.bind( null, id, postListStoreId ) ); } else { wpcom .me() - .postsList( params, PostActions.receivePage.bind( null, id ) ); + .postsList( params, PostActions.receivePage.bind( null, id, postListStoreId ) ); } }, - receivePage: function( id, error, data ) { + receivePage: function( id, postListStoreId, error, data ) { Dispatcher.handleServerAction( { type: 'RECEIVE_POSTS_PAGE', id: id, + postListStoreId: postListStoreId, error: error, data: data } ); }, - fetchUpdated: function() { - var id, params, siteID; + fetchUpdated: function( postListStoreId = 'default' ) { + var id, params, siteID, postListStore; + + postListStore = postListStoreFactory( postListStoreId ); Dispatcher.handleViewAction( { - type: 'FETCH_UPDATED_POSTS' + type: 'FETCH_UPDATED_POSTS', + postListStoreId: postListStoreId } ); - id = PostListStore.getID(); - params = PostListStore.getUpdatesParams(); - siteID = PostListStore.getSiteID(); + id = postListStore.getID(); + params = postListStore.getUpdatesParams(); + siteID = postListStore.getSiteID(); if ( siteID ) { debug( 'Fetching posts that have been updated for %s since %s %o', siteID, params.modified_after, params ); wpcom .site( siteID ) - .postsList( params, PostActions.receiveUpdated.bind( null, id ) ); + .postsList( params, PostActions.receiveUpdated.bind( null, id, postListStoreId ) ); } else { debug( 'Fetching posts that have been updated since %s %o', params.modified_after, params ); wpcom .me() - .postsList( params, PostActions.receiveUpdated.bind( null, id ) ); + .postsList( params, PostActions.receiveUpdated.bind( null, id, postListStoreId ) ); } }, - receiveUpdated: function( id, error, data ) { + receiveUpdated: function( id, postListStoreId, error, data ) { Dispatcher.handleServerAction( { type: 'RECEIVE_UPDATED_POSTS', id: id, + postListStoreId: postListStoreId, error: error, data: data } ); diff --git a/client/lib/posts/post-counts-store.js b/client/lib/posts/post-counts-store.js index 67ffc56072d97..d17afd4ace58f 100644 --- a/client/lib/posts/post-counts-store.js +++ b/client/lib/posts/post-counts-store.js @@ -11,7 +11,7 @@ var sum = require( 'lodash/math/sum' ), * Internal dependencies */ var emitter = require( 'lib/mixins/emitter' ), - PostListStore = require( './post-list-store' ), + PostListStore = require( './post-list-store-factory' )(), PostsStore = require( './posts-store' ), sites = require( 'lib/sites-list' )(), postUtils = require( 'lib/posts/utils' ), diff --git a/client/lib/posts/post-list-cache-store.js b/client/lib/posts/post-list-cache-store.js index 78285a6918988..c7c2dd572ebaa 100644 --- a/client/lib/posts/post-list-cache-store.js +++ b/client/lib/posts/post-list-cache-store.js @@ -100,7 +100,7 @@ var PostsListCache = { PostsListCache.dispatchToken = Dispatcher.register( function( payload ) { var action = payload.action, - PostListStore = require( './post-list-store' ); + PostListStore = require( './post-list-store-factory' )(); Dispatcher.waitFor( [ PostListStore.dispatchToken ] ); diff --git a/client/lib/posts/post-list-store-factory.js b/client/lib/posts/post-list-store-factory.js new file mode 100644 index 0000000000000..be2ab637946a7 --- /dev/null +++ b/client/lib/posts/post-list-store-factory.js @@ -0,0 +1,24 @@ +/** + * Internal Dependencies + **/ +import PostListStore from './post-list-store'; + +/** + * Module variables + **/ +const _postListStores = {}; + +export default function( storeId ) { + const postStoreId = storeId || 'default'; + let postListStore = _postListStores[ postStoreId ]; + + if ( postListStore ) { + return postListStore; + } + + postListStore = new PostListStore( postStoreId ); + + _postListStores[ postStoreId ] = postListStore; + + return postListStore; +} diff --git a/client/lib/posts/post-list-store.js b/client/lib/posts/post-list-store.js index 248486a7ca6e1..d14b636a87eb6 100644 --- a/client/lib/posts/post-list-store.js +++ b/client/lib/posts/post-list-store.js @@ -1,24 +1,29 @@ /** * External dependencies */ -var debug = require( 'debug' )( 'calypso:posts-list' ), - clone = require( 'lodash/lang/clone' ), - some = require( 'lodash/collection/some' ), - assign = require( 'lodash/object/assign' ), - transform = require( 'lodash/object/transform' ), - difference = require( 'lodash/array/difference' ), - last = require( 'lodash/array/last' ), - max = require( 'lodash/collection/max' ); +import debugFactory from 'debug'; +const debug = debugFactory( 'calypso:posts-list' ); +import clone from 'lodash/lang/clone'; +import assign from 'lodash/object/assign'; +import transform from 'lodash/object/transform'; +import difference from 'lodash/array/difference'; +import last from 'lodash/array/last'; +import max from 'lodash/collection/max'; +import { EventEmitter } from 'events/'; +import some from 'lodash/collection/some' /** * Internal dependencies */ -var Emitter = require( 'lib/mixins/emitter' ), - Dispatcher = require( 'dispatcher' ), - treeConvert = require( 'lib/tree-convert' )( 'ID' ), - PostsStore = require( './posts-store' ); +import Dispatcher from 'dispatcher'; +import treeConvert from 'lib/tree-convert'; +import PostsStore from './posts-store'; +import PostListCacheStore from './post-list-cache-store'; -var _defaultQuery = { +/** + * Module Variables + */ +const _defaultQuery = { siteID: false, type: 'post', status: 'publish', @@ -29,355 +34,361 @@ var _defaultQuery = { perPage: 20 }; -var _activeList = { - postIds: [], - errors: [], - query: clone( _defaultQuery ), - page: 0, - nextPageHandle: false, - isLastPage: false, - isFetchingNextPage: false, - isFetchingUpdated: false -}; +let _nextId = 0; -var _nextId = 0; -var PostListStore; +module.exports = function( id ) { + if ( ! id ) { + throw new Error( 'must supply a post-list-store id' ); + } -function queryPosts( options ) { - var query = assign( {}, _defaultQuery, options ), - cache = require( './post-list-cache-store' ), - list; + let _activeList = { + postIds: [], + errors: [], + query: clone( _defaultQuery ), + page: 0, + nextPageHandle: false, + isLastPage: false, + isFetchingNextPage: false, + isFetchingUpdated: false + }; + + function queryPosts( options ) { + let query = assign( {}, _defaultQuery, options ); + + if ( query.siteID && typeof query.siteID === 'string' ) { + query.siteID = query.siteID.replace( /::/g, '/' ); + } - if ( query.siteID && typeof query.siteID === 'string' ) { - query.siteID = query.siteID.replace( /::/g, '/' ); - } + if ( query.status === 'draft,pending' ) { + query.orderBy = 'modified'; + } - if ( query.status === 'draft,pending' ) { - query.orderBy = 'modified'; + let list = PostListCacheStore.get( query ); + + if ( list ) { + _activeList = list; + } else { + _activeList = { + id: _nextId, + postIds: [], + errors: [], + query: query, + page: 0, + isLastPage: false, + isFetchingNextPage: false, + isFetchingUpdated: false + }; + + _nextId++; + } } - list = cache.get( query ); - - if ( list ) { - _activeList = list; - } else { - _activeList = { - id: _nextId, - postIds: [], - errors: [], - query: query, - page: 0, - isLastPage: false, - isFetchingNextPage: false, - isFetchingUpdated: false - }; - - _nextId++; + /** + * Remove any keys from the params that are null or undefined + * + * We do this to avoid sending empty values along. + * Returns a new object representing the clean params. + * The original params is unmodified. + * + * @param {string} params The params to clean + * @return {object} The cleaned params object. + */ + function cleanParams( params ) { + return transform( params, function( result, value, key ) { + if ( value != null ) { + result[ key ] = value; + } + }, {} ); } - PostListStore.emit( 'change' ); -} - -/** - * Remove any keys from the params that are null or undefined - * - * We do this to avoid sending empty values along. - * Returns a new object representing the clean params. - * The original params is unmodified. - * - * @param {string} params The params to clean - * @return {object} The cleaned params object. - */ -function cleanParams( params ) { - return transform( params, function( result, value, key ) { - if ( value != null ) { - result[ key ] = value; - } - }, {} ); -} + /** + * Sort the active list + **/ + function sort() { + var key = _activeList.query.orderBy; + + _activeList.postIds.sort( function( a, b ) { + var postA = PostsStore.get( a ), + postB = PostsStore.get( b ), + timeA = postA[ key ], + timeB = postB[ key ]; + + if ( timeA === timeB ) { + if ( postA.title === postB.title ) { + return 0; + } -/** - * Sort the active list - **/ -function sort() { - var key = _activeList.query.orderBy; - - _activeList.postIds.sort( function( a, b ) { - var postA = PostsStore.get( a ), - postB = PostsStore.get( b ), - timeA = postA[ key ], - timeB = postB[ key ]; - - if ( timeA === timeB ) { - if ( postA.title === postB.title ) { - return 0; - } else { return postA.title > postB.title ? 1 : -1; } - } - // reverse-chronological - return timeA > timeB ? -1 : 1; - } ); -} - -/** - * Process a new page of data and concatenate to the end of the list - **/ -function receivePage( id, error, data ) { - var found = data && data.found, - posts, - postIds; - - if ( id !== _activeList.id ) { - return; + // reverse-chronological + return timeA > timeB ? -1 : 1; + } ); } - _activeList.isFetchingNextPage = false; + // Process a new page of data and concatenate to the end of the list + function receivePage( listId, error, data ) { + const found = data && data.found; + let posts; + let postIds; - if ( error ) { - debug( 'Error fetching PostsList from api:', error ); - error.timestamp = Date.now(); - _activeList.errors.push( error ); - PostListStore.emit( 'change' ); - return; - } + if ( listId !== _activeList.id ) { + return; + } - if ( ! found ) { - _activeList.isLastPage = true; - PostListStore.emit( 'change' ); - return; - } + _activeList.isFetchingNextPage = false; - // if we got a next page handle, cache it for the next page - _activeList.nextPageHandle = data.meta && data.meta.next_page; + if ( error ) { + debug( 'Error fetching PostsList from api:', error ); + error.timestamp = Date.now(); + _activeList.errors.push( error ); + return; + } - if ( ! _activeList.nextPageHandle ) { - _activeList.isLastPage = true; - } + if ( ! found ) { + _activeList.isLastPage = true; + return; + } - posts = data.posts; + // if we got a next page handle, cache it for the next page + _activeList.nextPageHandle = data.meta && data.meta.next_page; + + if ( ! _activeList.nextPageHandle ) { + _activeList.isLastPage = true; + } - postIds = posts.map( function( post ) { - return post.global_ID; - } ); + posts = data.posts; + + postIds = posts.map( function( post ) { + return post.global_ID; + } ); - if ( postIds.length ) { - // did we actually find any new posts? - postIds = difference( postIds, _activeList.postIds ); if ( postIds.length ) { - _activeList.postIds = _activeList.postIds.concat( postIds ); - _activeList.page++; + // did we actually find any new posts? + postIds = difference( postIds, _activeList.postIds ); + if ( postIds.length ) { + _activeList.postIds = _activeList.postIds.concat( postIds ); + _activeList.page++; + } } } - PostListStore.emit( 'change' ); -} + // Merge updated posts + function receiveUpdates( listId, error, data ) { + let posts; + let postIds; + let newPostIds; -/** - * Merge updated posts - **/ -function receiveUpdates( id, error, data ) { - var posts, postIds, newPostIds; - - if ( error ) { - debug( 'An error occurred while fetching updated posts %o', error ); - return; - } + if ( error ) { + debug( 'An error occurred while fetching updated posts %o', error ); + return; + } - if ( id !== _activeList.id ) { - return; - } + if ( listId !== _activeList.id ) { + return; + } - if ( ! data.posts.length ) { - return; - } + if ( ! data.posts.length ) { + return; + } - posts = data.posts; + posts = data.posts; - debug( 'Fetched updated posts:', posts ); + debug( 'Fetched updated posts:', posts ); - postIds = posts.map( function( post ) { - return post.global_ID; - } ); + postIds = posts.map( function( post ) { + return post.global_ID; + } ); - newPostIds = difference( postIds, _activeList.postIds ); + newPostIds = difference( postIds, _activeList.postIds ); - if ( newPostIds.length ) { - _activeList.postIds = _activeList.postIds.concat( newPostIds ); - sort( _activeList.postIds ); + if ( newPostIds.length ) { + _activeList.postIds = _activeList.postIds.concat( newPostIds ); + sort( _activeList.postIds ); + } } - PostListStore.emit( 'change' ); -} - -PostListStore = { - - get: function() { - return _activeList; - }, + return new class extends EventEmitter { + constructor() { + super(); + this.id = id; + this.dispatchToken = Dispatcher.register( this.handlePayload.bind( this ) ); + } - getID: function() { - return _activeList.id; - }, + get() { + return _activeList; + } - getSiteID: function() { - return _activeList.query.siteID; - }, + getID() { + return _activeList.id; + } - /** - * Get list of posts from current object - */ - getAll: function() { - return _activeList.postIds.map( function( globalID ) { - return PostsStore.get( globalID ); - } ); - }, + getSiteID() { + return _activeList.query.siteID; + } - getTree: function() { - var sortedPosts = []; + // Get list of posts from current object + getAll() { + return _activeList.postIds.map( function( globalID ) { + return PostsStore.get( globalID ); + } ); + } - // clone objects to prevent mutating store data, set parent to number - _activeList.postIds.forEach( function( globalID ) { - var post = clone( PostsStore.get( globalID ) ); - post.parent = post.parent ? post.parent.ID : 0; - sortedPosts.push( post ); - } ); + getTree() { + const sortedPosts = []; - return treeConvert.treeify( sortedPosts ); - }, + // clone objects to prevent mutating store data, set parent to number + _activeList.postIds.forEach( function( globalID ) { + let post = clone( PostsStore.get( globalID ) ); + post.parent = post.parent ? post.parent.ID : 0; + sortedPosts.push( post ); + } ); - getPost: function( globalID ) { - if ( _activeList.postIds.indexOf( globalID ) > -1 ) { - return PostsStore.get( globalID ); + return treeConvert( 'ID' ).treeify( sortedPosts ); } - }, - getPage: function() { - return _activeList.page; - }, - - isLastPage: function() { - return _activeList.isLastPage; - }, + getPost( globalID ) { + if ( _activeList.postIds.indexOf( globalID ) > -1 ) { + return PostsStore.get( globalID ); + } + } - isFetchingNextPage: function() { - return _activeList.isFetchingNextPage; - }, + getPage() { + return _activeList.page; + } - // Have we received an error recently? - hasRecentError: function() { - const recentTimeIntervalSeconds = 30; - const dateNow = Date.now(); + off( event, method ) { + this.removeListener( event, method ); + } - return some( _activeList.errors, function( error ) { - return ( dateNow - error.timestamp ) < ( recentTimeIntervalSeconds * 1000 ); - } ); - }, - - getNextPageParams: function() { - var params = {}, - query = _activeList.query; - - params.status = query.status; - params.order_by = query.orderBy; - params.order = query.order; - params.author = query.author; - params.number = query.perPage; - params.type = query.type; - params.page_handle = _activeList.nextPageHandle; - params.exclude_tree = query.exclude_tree; - params.number = query.number; - params.before = query.before; - params.after = query.after; - - if ( query.search ) { - params.search = query.search; + isLastPage() { + return _activeList.isLastPage; } - if ( ! params.siteID ) { - // Only query from visible sites - params.site_visibility = 'visible'; + isFetchingNextPage() { + return _activeList.isFetchingNextPage; } - if ( query.meta ) { - params.meta = query.meta; + // Have we received an error recently? + hasRecentError() { + const recentTimeIntervalSeconds = 30; + const dateNow = Date.now(); + + return some( _activeList.errors, function( error ) { + return ( dateNow - error.timestamp ) < ( recentTimeIntervalSeconds * 1000 ); + } ); } - return cleanParams( params ); - }, + getNextPageParams() { + let params = {}; + const query = _activeList.query; + + params.status = query.status; + params.order_by = query.orderBy; + params.order = query.order; + params.author = query.author; + params.number = query.perPage; + params.type = query.type; + params.page_handle = _activeList.nextPageHandle; + params.exclude_tree = query.exclude_tree; + params.number = query.number; + params.before = query.before; + params.after = query.after; + + if ( query.search ) { + params.search = query.search; + } - getUpdatesParams: function() { - var params = {}, - query = _activeList.query; + if ( ! params.siteID ) { + // Only query from visible sites + params.site_visibility = 'visible'; + } - params.status = query.status; - params.author = query.author; - params.type = query.type; + if ( query.meta ) { + params.meta = query.meta; + } - if ( query.search ) { - params.search = query.search; + return cleanParams( params ); } - if ( ! params.siteID ) { - // Only query from visible sites - params.site_visibility = 'visible'; - } + getUpdatesParams() { + let params = {}; + const query = _activeList.query; - if ( query.meta ) { - params.meta = query.meta; - } + params.status = query.status; + params.author = query.author; + params.type = query.type; - if ( _activeList.postIds.length ) { - params.modified_after = max( PostListStore.getAll(), function( post ) { - return new Date( post.modified ).getTime(); - } ).modified; + if ( query.search ) { + params.search = query.search; + } - // For situations where the list ordered by publish date, we want to - // only get updates that should show up in the list to avoid creating - // a gap in our paging - if ( query.orderBy !== 'modified' && ! _activeList.isLastPage ) { - params.after = PostListStore.getPost( last( _activeList.postIds ) ).date; + if ( ! params.siteID ) { + // Only query from visible sites + params.site_visibility = 'visible'; } - } - return cleanParams( params ); - } + if ( query.meta ) { + params.meta = query.meta; + } -}; + if ( _activeList.postIds.length ) { + params.modified_after = max( this.getAll(), function( post ) { + return new Date( post.modified ).getTime(); + } ).modified; -Emitter( PostListStore ); - -PostListStore.dispatchToken = Dispatcher.register( function( payload ) { - var action = payload.action; - - Dispatcher.waitFor( [ PostsStore.dispatchToken ] ); - - switch( action.type ) { - case 'QUERY_POSTS': - queryPosts( action.options ); - break; - case 'FETCH_NEXT_POSTS_PAGE': - _activeList.isFetchingNextPage = true; - PostListStore.emit( 'change' ); - break; - case 'RECEIVE_POSTS_PAGE': - receivePage( action.id, action.error, action.data ); - break; - - case 'RECEIVE_UPDATED_POSTS': - receiveUpdates( action.id, action.error, action.data ); - break; - - case 'RECEIVE_UPDATED_POST': - if ( action.post ) { - if ( _activeList.postIds.indexOf( action.post.global_ID ) > -1 ) { - PostListStore.emit( 'change' ); + // For situations where the list ordered by publish date, we want to + // only get updates that should show up in the list to avoid creating + // a gap in our paging + if ( query.orderBy !== 'modified' && ! _activeList.isLastPage ) { + params.after = this.getPost( last( _activeList.postIds ) ).date; } } - break; - } + return cleanParams( params ); + } -} ); + handlePayload( payload ) { + const action = payload.action; -module.exports = PostListStore; + // If this action does not match this post-list-store.id return, but always evaluate RECEIVE_UPDATED_POST regardless + if ( ( action.postListStoreId && action.postListStoreId !== this.id ) && + 'RECEIVE_UPDATED_POST' !== action.type + ) { + return; + } + + Dispatcher.waitFor( [ PostsStore.dispatchToken ] ); + + switch ( action.type ) { + case 'QUERY_POSTS': + debug( 'QUERY_POSTS', action ); + queryPosts( action.options ); + this.emit( 'change' ); + break; + case 'FETCH_NEXT_POSTS_PAGE': + debug( 'FETCH_NEXT_POSTS_PAGE', action ); + _activeList.isFetchingNextPage = true; + this.emit( 'change' ); + break; + case 'RECEIVE_POSTS_PAGE': + debug( 'receivePage', action ); + receivePage( action.id, action.error, action.data ); + this.emit( 'change' ); + break; + + case 'RECEIVE_UPDATED_POSTS': + receiveUpdates( action.id, action.error, action.data ); + this.emit( 'change' ); + break; + + case 'RECEIVE_UPDATED_POST': + if ( action.post ) { + if ( _activeList.postIds.indexOf( action.post.global_ID ) > -1 ) { + this.emit( 'change' ); + } + } + break; + } + } + }(); +}; diff --git a/client/lib/posts/test/post-list-store.js b/client/lib/posts/test/post-list-store.js new file mode 100644 index 0000000000000..ce748dc0f1d56 --- /dev/null +++ b/client/lib/posts/test/post-list-store.js @@ -0,0 +1,118 @@ +/* eslint-disable vars-on-top */ +require( 'lib/react-test-env-setup' )(); + +/** + * External dependencies + */ +var rewire = require( 'rewire' ), + assert = require( 'chai' ).assert, + includes = require( 'lodash/collection/includes' ), + Dispatcher = require( 'dispatcher' ); + +describe( 'post-list-store', function() { + var PostListStoreFactory, defaultPostListStore; + before( function() { + PostListStoreFactory = rewire( '../post-list-store-factory' ); + } ); + + beforeEach( function() { + PostListStoreFactory.__set__( '_postListStores', {} ); + defaultPostListStore = PostListStoreFactory(); + } ); + + afterEach( function() { + if ( defaultPostListStore.dispatchToken ) { + Dispatcher.unregister( defaultPostListStore.dispatchToken ); + } + } ); + + describe( 'postListStoreId', function() { + it( 'should set the default postListStoreId', function() { + assert.equal( defaultPostListStore.id, 'default' ); + } ); + } ); + + describe( 'dispatcher', function() { + it( 'should have a dispatch token', function() { + assert.typeOf( defaultPostListStore.dispatchToken, 'string' ); + } ); + } ); + + describe( '#get', function() { + it( 'should return an object', function() { + assert.instanceOf( defaultPostListStore.get(), Object ); + } ); + + it( 'should return the correct default values', function(){ + var postList = defaultPostListStore.get(); + assert.instanceOf( postList.postIds, Array ); + assert.equal( postList.postIds.length, 0 ); + assert.instanceOf( postList.errors, Array ); + assert.instanceOf( postList.query, Object ); + assert.equal( postList.errors.length, 0 ); + assert.equal( postList.page, 0 ); + assert.isFalse( postList.nextPageHandle ); + assert.isFalse( postList.isLastPage ); + assert.isFalse( postList.isFetchingNextPage ); + assert.isFalse( postList.isFetchingUpdated ); + } ); + } ); + + describe( '#getId', function() { + it( 'should the return list ID', function() { + assert.equal( defaultPostListStore.getID(), defaultPostListStore.get().id ); + } ); + } ); + + describe( '#getSiteID', function() { + it( 'should the return site ID', function() { + assert.equal( defaultPostListStore.getSiteID(), defaultPostListStore.get().query.siteID ); + } ); + } ); + + describe( '#getAll', function() { + it( 'should return an array of posts', function() { + var allPosts = defaultPostListStore.getAll(); + assert.instanceOf( allPosts, Array ); + assert.equal( allPosts.length, 0 ); + } ); + } ); + + describe( '#getTree', function() { + it( 'should return a tree-ified array of posts', function() { + var treePosts = defaultPostListStore.getTree(); + assert.instanceOf( treePosts, Array ); + assert.equal( treePosts.length, 0 ); + } ); + } ); + + describe( '#getPage', function() { + it( 'should return the page number', function() { + assert.equal( defaultPostListStore.getPage(), defaultPostListStore.get().page ); + } ); + } ); + + describe( '#isLastPage', function() { + it( 'should return isLastPage boolean', function() { + assert.equal( defaultPostListStore.isLastPage(), defaultPostListStore.get().isLastPage ); + } ); + } ); + + describe( '#isFetchingNextPage', function() { + it( 'should return isFetchingNextPage boolean', function() { + assert.equal( defaultPostListStore.isFetchingNextPage(), defaultPostListStore.get().isFetchingNextPage ); + } ); + } ); + + describe( '#getNextPageParams', function() { + it( 'should return an object of params', function() { + assert.instanceOf( defaultPostListStore.getNextPageParams(), Object ); + } ); + } ); + + describe( '#getUpdatesParams', function() { + it( 'should return an object of params', function() { + assert.instanceOf( defaultPostListStore.getUpdatesParams(), Object ); + } ); + } ); +} ); diff --git a/client/my-sites/stats/post-performance/index.jsx b/client/my-sites/stats/post-performance/index.jsx index 5b90241e72ee1..5762e6abef944 100644 --- a/client/my-sites/stats/post-performance/index.jsx +++ b/client/my-sites/stats/post-performance/index.jsx @@ -9,7 +9,7 @@ var React = require( 'react/addons' ), * Internal dependencies */ var Card = require( 'components/card' ), - PostListStore = require( 'lib/posts/post-list-store' ), + PostListStore = require( 'lib/posts/post-list-store-factory' )(), PostStatsStore = require( 'lib/post-stats/store' ), Emojify = require( 'components/emojify' ), actions = require( 'lib/posts/actions' ), From 46753d467cf682c14fa7a370d3f9c7203110fe3a Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Tue, 24 Nov 2015 16:46:42 -0800 Subject: [PATCH 10/89] Lint fixes in tests. --- client/lib/posts/test/post-list-store.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/lib/posts/test/post-list-store.js b/client/lib/posts/test/post-list-store.js index ce748dc0f1d56..b0eac3a2bc81c 100644 --- a/client/lib/posts/test/post-list-store.js +++ b/client/lib/posts/test/post-list-store.js @@ -6,18 +6,17 @@ require( 'lib/react-test-env-setup' )(); */ var rewire = require( 'rewire' ), assert = require( 'chai' ).assert, - includes = require( 'lodash/collection/includes' ), Dispatcher = require( 'dispatcher' ); describe( 'post-list-store', function() { - var PostListStoreFactory, defaultPostListStore; + var postListStoreFactory, defaultPostListStore; before( function() { - PostListStoreFactory = rewire( '../post-list-store-factory' ); + postListStoreFactory = rewire( '../post-list-store-factory' ); } ); beforeEach( function() { - PostListStoreFactory.__set__( '_postListStores', {} ); - defaultPostListStore = PostListStoreFactory(); + postListStoreFactory.__set__( '_postListStores', {} ); + defaultPostListStore = postListStoreFactory(); } ); afterEach( function() { @@ -43,7 +42,7 @@ describe( 'post-list-store', function() { assert.instanceOf( defaultPostListStore.get(), Object ); } ); - it( 'should return the correct default values', function(){ + it( 'should return the correct default values', function() { var postList = defaultPostListStore.get(); assert.instanceOf( postList.postIds, Array ); assert.equal( postList.postIds.length, 0 ); From 0ee0e7c65a80be200097e78242abf78ed8f01a8b Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Wed, 25 Nov 2015 07:49:18 -0800 Subject: [PATCH 11/89] PostListStore: Additional test coverage for factories. --- client/lib/posts/test/post-list-store.js | 126 ++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/client/lib/posts/test/post-list-store.js b/client/lib/posts/test/post-list-store.js index b0eac3a2bc81c..df305314f1530 100644 --- a/client/lib/posts/test/post-list-store.js +++ b/client/lib/posts/test/post-list-store.js @@ -8,6 +8,43 @@ var rewire = require( 'rewire' ), assert = require( 'chai' ).assert, Dispatcher = require( 'dispatcher' ); +/** + * Mock Data + */ +var TWO_POST_PAYLOAD = { + found: 2, + posts: [ { + global_ID: 778 + }, { + global_ID: 779 + } ] +}; +var DEFAULT_POST_LIST_ID = 'default'; + +function dispatchReceivePostsPage( id, postListStoreId, data ) { + var mockData = { + found: 1, + posts: [ { + global_ID: 777 + } ] + }; + Dispatcher.handleServerAction( { + type: 'RECEIVE_POSTS_PAGE', + id: id, + postListStoreId: postListStoreId, + data: data || mockData, + error: null + } ); +} + +function dispatchQueryPosts( postListStoreId, options ) { + Dispatcher.handleViewAction( { + type: 'QUERY_POSTS', + options: options, + postListStoreId: postListStoreId + } ); +} + describe( 'post-list-store', function() { var postListStoreFactory, defaultPostListStore; before( function() { @@ -27,7 +64,7 @@ describe( 'post-list-store', function() { describe( 'postListStoreId', function() { it( 'should set the default postListStoreId', function() { - assert.equal( defaultPostListStore.id, 'default' ); + assert.equal( defaultPostListStore.id, DEFAULT_POST_LIST_ID ); } ); } ); @@ -114,4 +151,91 @@ describe( 'post-list-store', function() { assert.instanceOf( defaultPostListStore.getUpdatesParams(), Object ); } ); } ); + + describe( 'RECEIVE_POSTS_PAGE', function() { + it( 'should add post ids for matching postListStore', function() { + dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); + assert.equal( defaultPostListStore.getAll().length, 1 ); + } ); + + it( 'should add additional post ids for matching postListStore', function() { + dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); + assert.equal( defaultPostListStore.getAll().length, 1 ); + dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id, TWO_POST_PAYLOAD ); + assert.equal( defaultPostListStore.getAll().length, 3 ); + } ); + + it( 'should add only unique post.global_IDs postListStore', function() { + dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); + assert.equal( defaultPostListStore.getAll().length, 1 ); + dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); + assert.equal( defaultPostListStore.getAll().length, 1 ); + } ); + + it( 'should not update if cached store id does not match', function() { + dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); + assert.equal( defaultPostListStore.getAll().length, 1 ); + dispatchReceivePostsPage( 999, defaultPostListStore.id ); + assert.equal( defaultPostListStore.getAll().length, 1 ); + } ); + + it( 'should not add post ids if postListStore does not match', function() { + dispatchReceivePostsPage( defaultPostListStore.getID(), 'some-other-post-list-store' ); + assert.equal( defaultPostListStore.getAll().length, 0 ); + } ); + + it( 'should set isLastPage true if no next page handle returned', function() { + dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); + assert.isTrue( defaultPostListStore.isLastPage() ); + } ); + } ); + + describe( 'QUERY_POSTS', function() { + it( 'should not change cached list if query does not change', function() { + var currentCacheId = defaultPostListStore.getID(); + dispatchQueryPosts( DEFAULT_POST_LIST_ID, {} ); + assert.equal( currentCacheId, defaultPostListStore.getID() ); + } ); + + it( 'should change the active query and id when query options change', function() { + var currentCacheId = defaultPostListStore.getID(); + dispatchQueryPosts( DEFAULT_POST_LIST_ID, { type: 'page' } ); + assert.notEqual( currentCacheId, defaultPostListStore.getID() ); + } ); + + it( 'should set site_visibility if no siteID is present', function() { + dispatchQueryPosts( DEFAULT_POST_LIST_ID, { type: 'page' } ); + assert.equal( defaultPostListStore.getNextPageParams().site_visibility, 'visible' ); + } ); + + it( 'should set query options passed in', function() { + var params; + dispatchQueryPosts( DEFAULT_POST_LIST_ID, { + type: 'page', + order: 'ASC' + } ); + params = defaultPostListStore.getNextPageParams(); + assert.equal( params.type, 'page' ); + assert.equal( params.order, 'ASC' ); + } ); + + it( 'should remove null query options passed in', function() { + var params; + dispatchQueryPosts( DEFAULT_POST_LIST_ID, { + type: 'page', + order: 'ASC', + search: null + } ); + params = defaultPostListStore.getNextPageParams(); + assert.isUndefined( params.search ); + } ); + + it( 'should not set query options on a different postListStore instance', function() { + var params; + dispatchQueryPosts( 'some-other-post-list-store', { type: 'page' } ); + params = defaultPostListStore.getNextPageParams(); + assert.equal( params.type, 'post' ); + assert.equal( params.order, 'DESC' ); + } ); + } ); } ); From a6bcd5bae319ad955ba9dbccfd72d23d4d8b0057 Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Wed, 25 Nov 2015 07:49:48 -0800 Subject: [PATCH 12/89] Lint fixes. --- client/lib/posts/test/post-list-store.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/lib/posts/test/post-list-store.js b/client/lib/posts/test/post-list-store.js index df305314f1530..abd2c7458277c 100644 --- a/client/lib/posts/test/post-list-store.js +++ b/client/lib/posts/test/post-list-store.js @@ -23,11 +23,11 @@ var DEFAULT_POST_LIST_ID = 'default'; function dispatchReceivePostsPage( id, postListStoreId, data ) { var mockData = { - found: 1, - posts: [ { - global_ID: 777 - } ] - }; + found: 1, + posts: [ { + global_ID: 777 + } ] + }; Dispatcher.handleServerAction( { type: 'RECEIVE_POSTS_PAGE', id: id, From 33c1eb16842a14575fe5b18f5d30eefd5d16e24a Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Wed, 25 Nov 2015 13:46:22 -0800 Subject: [PATCH 13/89] Updates per feedback. --- client/lib/posts/post-list-store-factory.js | 11 +- client/lib/posts/post-list-store.js | 2 +- client/lib/posts/test/post-list-store.js | 149 ++++++++++---------- 3 files changed, 81 insertions(+), 81 deletions(-) diff --git a/client/lib/posts/post-list-store-factory.js b/client/lib/posts/post-list-store-factory.js index be2ab637946a7..0a991865c98fc 100644 --- a/client/lib/posts/post-list-store-factory.js +++ b/client/lib/posts/post-list-store-factory.js @@ -10,15 +10,10 @@ const _postListStores = {}; export default function( storeId ) { const postStoreId = storeId || 'default'; - let postListStore = _postListStores[ postStoreId ]; - if ( postListStore ) { - return postListStore; + if ( ! _postListStores[ postStoreId ] ) { + _postListStores[ postStoreId ] = new PostListStore( postStoreId ); } - postListStore = new PostListStore( postStoreId ); - - _postListStores[ postStoreId ] = postListStore; - - return postListStore; + return _postListStores[ postStoreId ]; } diff --git a/client/lib/posts/post-list-store.js b/client/lib/posts/post-list-store.js index d14b636a87eb6..998e8af74a73d 100644 --- a/client/lib/posts/post-list-store.js +++ b/client/lib/posts/post-list-store.js @@ -10,7 +10,7 @@ import difference from 'lodash/array/difference'; import last from 'lodash/array/last'; import max from 'lodash/collection/max'; import { EventEmitter } from 'events/'; -import some from 'lodash/collection/some' +import some from 'lodash/collection/some'; /** * Internal dependencies diff --git a/client/lib/posts/test/post-list-store.js b/client/lib/posts/test/post-list-store.js index abd2c7458277c..a53e01caac61b 100644 --- a/client/lib/posts/test/post-list-store.js +++ b/client/lib/posts/test/post-list-store.js @@ -1,17 +1,16 @@ -/* eslint-disable vars-on-top */ -require( 'lib/react-test-env-setup' )(); - /** * External dependencies */ -var rewire = require( 'rewire' ), - assert = require( 'chai' ).assert, - Dispatcher = require( 'dispatcher' ); +import rewire from 'rewire'; +import { assert } from 'chai'; +import Dispatcher from 'dispatcher'; +import isPlainObject from 'lodash/lang/isPlainObject'; +import isArray from 'lodash/lang/isArray'; /** * Mock Data */ -var TWO_POST_PAYLOAD = { +const TWO_POST_PAYLOAD = { found: 2, posts: [ { global_ID: 778 @@ -19,10 +18,10 @@ var TWO_POST_PAYLOAD = { global_ID: 779 } ] }; -var DEFAULT_POST_LIST_ID = 'default'; +const DEFAULT_POST_LIST_ID = 'default'; function dispatchReceivePostsPage( id, postListStoreId, data ) { - var mockData = { + const mockData = { found: 1, posts: [ { global_ID: 777 @@ -45,46 +44,47 @@ function dispatchQueryPosts( postListStoreId, options ) { } ); } -describe( 'post-list-store', function() { - var postListStoreFactory, defaultPostListStore; - before( function() { +describe( 'post-list-store', () => { + let postListStoreFactory; + let defaultPostListStore; + before( () => { postListStoreFactory = rewire( '../post-list-store-factory' ); } ); - beforeEach( function() { + beforeEach( () => { postListStoreFactory.__set__( '_postListStores', {} ); defaultPostListStore = postListStoreFactory(); } ); - afterEach( function() { + afterEach( () => { if ( defaultPostListStore.dispatchToken ) { Dispatcher.unregister( defaultPostListStore.dispatchToken ); } } ); - describe( 'postListStoreId', function() { - it( 'should set the default postListStoreId', function() { + describe( 'postListStoreId', () => { + it( 'should set the default postListStoreId', () => { assert.equal( defaultPostListStore.id, DEFAULT_POST_LIST_ID ); } ); } ); - describe( 'dispatcher', function() { - it( 'should have a dispatch token', function() { + describe( 'dispatcher', () => { + it( 'should have a dispatch token', () => { assert.typeOf( defaultPostListStore.dispatchToken, 'string' ); } ); } ); - describe( '#get', function() { - it( 'should return an object', function() { - assert.instanceOf( defaultPostListStore.get(), Object ); + describe( '#get', () => { + it( 'should return an object', () => { + assert.isTrue( isPlainObject( defaultPostListStore.get() ) ); } ); - it( 'should return the correct default values', function() { - var postList = defaultPostListStore.get(); - assert.instanceOf( postList.postIds, Array ); + it( 'should return the correct default values', () => { + const postList = defaultPostListStore.get(); + assert.isTrue( isArray( postList.postIds ) ); assert.equal( postList.postIds.length, 0 ); - assert.instanceOf( postList.errors, Array ); - assert.instanceOf( postList.query, Object ); + assert.isTrue( isArray( postList.errors ) ); + assert.isTrue( isPlainObject( postList.query ) ); assert.equal( postList.errors.length, 0 ); assert.equal( postList.page, 0 ); assert.isFalse( postList.nextPageHandle ); @@ -94,146 +94,151 @@ describe( 'post-list-store', function() { } ); } ); - describe( '#getId', function() { - it( 'should the return list ID', function() { + describe( '#getId', () => { + it( 'should the return list ID', () => { assert.equal( defaultPostListStore.getID(), defaultPostListStore.get().id ); } ); + + it( 'should globally increment ids across all stores', () => { + const anotherPostListStore = postListStoreFactory( 'post-lists-nom' ); + dispatchQueryPosts( defaultPostListStore.id ); + dispatchQueryPosts( anotherPostListStore.id ); + assert.equal( defaultPostListStore.getID() + 1, anotherPostListStore.getID() ); + } ); } ); - describe( '#getSiteID', function() { - it( 'should the return site ID', function() { + describe( '#getSiteID', () => { + it( 'should the return site ID', () => { assert.equal( defaultPostListStore.getSiteID(), defaultPostListStore.get().query.siteID ); } ); } ); - describe( '#getAll', function() { - it( 'should return an array of posts', function() { - var allPosts = defaultPostListStore.getAll(); - assert.instanceOf( allPosts, Array ); + describe( '#getAll', () => { + it( 'should return an array of posts', () => { + const allPosts = defaultPostListStore.getAll(); + assert.isTrue( isArray( allPosts ) ); assert.equal( allPosts.length, 0 ); } ); } ); - describe( '#getTree', function() { - it( 'should return a tree-ified array of posts', function() { - var treePosts = defaultPostListStore.getTree(); - assert.instanceOf( treePosts, Array ); + describe( '#getTree', () => { + it( 'should return a tree-ified array of posts', () => { + const treePosts = defaultPostListStore.getTree(); + assert.isTrue( isArray( treePosts ) ); assert.equal( treePosts.length, 0 ); } ); } ); - describe( '#getPage', function() { - it( 'should return the page number', function() { + describe( '#getPage', () => { + it( 'should return the page number', () => { assert.equal( defaultPostListStore.getPage(), defaultPostListStore.get().page ); } ); } ); - describe( '#isLastPage', function() { - it( 'should return isLastPage boolean', function() { + describe( '#isLastPage', () => { + it( 'should return isLastPage boolean', () => { assert.equal( defaultPostListStore.isLastPage(), defaultPostListStore.get().isLastPage ); } ); } ); - describe( '#isFetchingNextPage', function() { - it( 'should return isFetchingNextPage boolean', function() { + describe( '#isFetchingNextPage', () => { + it( 'should return isFetchingNextPage boolean', () => { assert.equal( defaultPostListStore.isFetchingNextPage(), defaultPostListStore.get().isFetchingNextPage ); } ); } ); - describe( '#getNextPageParams', function() { - it( 'should return an object of params', function() { - assert.instanceOf( defaultPostListStore.getNextPageParams(), Object ); + describe( '#getNextPageParams', () => { + it( 'should return an object of params', () => { + assert.isTrue( isPlainObject( defaultPostListStore.getNextPageParams() ) ); } ); } ); - describe( '#getUpdatesParams', function() { - it( 'should return an object of params', function() { - assert.instanceOf( defaultPostListStore.getUpdatesParams(), Object ); + describe( '#getUpdatesParams', () => { + it( 'should return an object of params', () => { + assert.isTrue( isPlainObject( defaultPostListStore.getUpdatesParams() ) ); } ); } ); - describe( 'RECEIVE_POSTS_PAGE', function() { - it( 'should add post ids for matching postListStore', function() { + describe( 'RECEIVE_POSTS_PAGE', () => { + it( 'should add post ids for matching postListStore', () => { dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); assert.equal( defaultPostListStore.getAll().length, 1 ); } ); - it( 'should add additional post ids for matching postListStore', function() { + it( 'should add additional post ids for matching postListStore', () => { dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); assert.equal( defaultPostListStore.getAll().length, 1 ); dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id, TWO_POST_PAYLOAD ); assert.equal( defaultPostListStore.getAll().length, 3 ); } ); - it( 'should add only unique post.global_IDs postListStore', function() { + it( 'should add only unique post.global_IDs postListStore', () => { dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); assert.equal( defaultPostListStore.getAll().length, 1 ); dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); assert.equal( defaultPostListStore.getAll().length, 1 ); } ); - it( 'should not update if cached store id does not match', function() { + it( 'should not update if cached store id does not match', () => { dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); assert.equal( defaultPostListStore.getAll().length, 1 ); dispatchReceivePostsPage( 999, defaultPostListStore.id ); assert.equal( defaultPostListStore.getAll().length, 1 ); } ); - it( 'should not add post ids if postListStore does not match', function() { + it( 'should not add post ids if postListStore does not match', () => { dispatchReceivePostsPage( defaultPostListStore.getID(), 'some-other-post-list-store' ); assert.equal( defaultPostListStore.getAll().length, 0 ); } ); - it( 'should set isLastPage true if no next page handle returned', function() { + it( 'should set isLastPage true if no next page handle returned', () => { dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); assert.isTrue( defaultPostListStore.isLastPage() ); } ); } ); - describe( 'QUERY_POSTS', function() { - it( 'should not change cached list if query does not change', function() { - var currentCacheId = defaultPostListStore.getID(); + describe( 'QUERY_POSTS', () => { + it( 'should not change cached list if query does not change', () => { + dispatchQueryPosts( DEFAULT_POST_LIST_ID, {} ); + const currentCacheId = defaultPostListStore.getID(); dispatchQueryPosts( DEFAULT_POST_LIST_ID, {} ); assert.equal( currentCacheId, defaultPostListStore.getID() ); } ); - it( 'should change the active query and id when query options change', function() { - var currentCacheId = defaultPostListStore.getID(); + it( 'should change the active query and id when query options change', () => { + const currentCacheId = defaultPostListStore.getID(); dispatchQueryPosts( DEFAULT_POST_LIST_ID, { type: 'page' } ); assert.notEqual( currentCacheId, defaultPostListStore.getID() ); } ); - it( 'should set site_visibility if no siteID is present', function() { + it( 'should set site_visibility if no siteID is present', () => { dispatchQueryPosts( DEFAULT_POST_LIST_ID, { type: 'page' } ); assert.equal( defaultPostListStore.getNextPageParams().site_visibility, 'visible' ); } ); - it( 'should set query options passed in', function() { - var params; + it( 'should set query options passed in', () => { dispatchQueryPosts( DEFAULT_POST_LIST_ID, { type: 'page', order: 'ASC' } ); - params = defaultPostListStore.getNextPageParams(); + const params = defaultPostListStore.getNextPageParams(); assert.equal( params.type, 'page' ); assert.equal( params.order, 'ASC' ); } ); - it( 'should remove null query options passed in', function() { - var params; + it( 'should remove null query options passed in', () => { dispatchQueryPosts( DEFAULT_POST_LIST_ID, { type: 'page', order: 'ASC', search: null } ); - params = defaultPostListStore.getNextPageParams(); + const params = defaultPostListStore.getNextPageParams(); assert.isUndefined( params.search ); } ); - it( 'should not set query options on a different postListStore instance', function() { - var params; + it( 'should not set query options on a different postListStore instance', () => { dispatchQueryPosts( 'some-other-post-list-store', { type: 'page' } ); - params = defaultPostListStore.getNextPageParams(); + const params = defaultPostListStore.getNextPageParams(); assert.equal( params.type, 'post' ); assert.equal( params.order, 'DESC' ); } ); From f7cb8137a971ec96328d67406d0377a4bdd497b9 Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Mon, 7 Dec 2015 13:13:53 -0800 Subject: [PATCH 14/89] Fixing test after rebase. --- client/lib/posts/test/post-list-store.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/lib/posts/test/post-list-store.js b/client/lib/posts/test/post-list-store.js index a53e01caac61b..293edf408da20 100644 --- a/client/lib/posts/test/post-list-store.js +++ b/client/lib/posts/test/post-list-store.js @@ -101,8 +101,14 @@ describe( 'post-list-store', () => { it( 'should globally increment ids across all stores', () => { const anotherPostListStore = postListStoreFactory( 'post-lists-nom' ); - dispatchQueryPosts( defaultPostListStore.id ); - dispatchQueryPosts( anotherPostListStore.id ); + dispatchQueryPosts( defaultPostListStore.id, { + type: 'page', + order: 'ASC' + } ); + dispatchQueryPosts( anotherPostListStore.id, { + type: 'page', + order: 'ASC' + } ); assert.equal( defaultPostListStore.getID() + 1, anotherPostListStore.getID() ); } ); } ); From 054f0117837910f111ee46829700bc448baaacb0 Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Mon, 7 Dec 2015 13:22:30 -0800 Subject: [PATCH 15/89] Add tests for hasRecentError --- client/lib/posts/test/post-list-store.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client/lib/posts/test/post-list-store.js b/client/lib/posts/test/post-list-store.js index 293edf408da20..4303f1a492373 100644 --- a/client/lib/posts/test/post-list-store.js +++ b/client/lib/posts/test/post-list-store.js @@ -165,6 +165,26 @@ describe( 'post-list-store', () => { } ); } ); + describe( '#hasRecentError', () => { + it( 'should return false if there are no errors', () => { + dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); + assert.isFalse( defaultPostListStore.hasRecentError() ); + } ); + + it( 'should return true if recent payload had error', () => { + Dispatcher.handleServerAction( { + type: 'RECEIVE_POSTS_PAGE', + id: defaultPostListStore.getID(), + postListStoreId: defaultPostListStore.id, + data: null, + error: { + omg: 'error!' + } + } ); + assert.isTrue( defaultPostListStore.hasRecentError() ); + } ); + } ); + describe( 'RECEIVE_POSTS_PAGE', () => { it( 'should add post ids for matching postListStore', () => { dispatchReceivePostsPage( defaultPostListStore.getID(), defaultPostListStore.id ); From 5cf488f74c1499e7424614d4d527f02ea8505f7b Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Mon, 7 Dec 2015 13:25:11 -0800 Subject: [PATCH 16/89] One last es6-ification and renaming and ordering of debug module --- client/lib/posts/post-list-store.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/lib/posts/post-list-store.js b/client/lib/posts/post-list-store.js index 998e8af74a73d..ddca5e778479a 100644 --- a/client/lib/posts/post-list-store.js +++ b/client/lib/posts/post-list-store.js @@ -1,8 +1,7 @@ /** * External dependencies */ -import debugFactory from 'debug'; -const debug = debugFactory( 'calypso:posts-list' ); +import debugModule from 'debug'; import clone from 'lodash/lang/clone'; import assign from 'lodash/object/assign'; import transform from 'lodash/object/transform'; @@ -34,9 +33,11 @@ const _defaultQuery = { perPage: 20 }; +const debug = debugModule( 'calypso:posts-list' ); + let _nextId = 0; -module.exports = function( id ) { +export default function( id ) { if ( ! id ) { throw new Error( 'must supply a post-list-store id' ); } From 3c571fecc5e6964149945ea00dfb6b02e686e43c Mon Sep 17 00:00:00 2001 From: Eric Binnion Date: Mon, 7 Dec 2015 17:14:58 -0600 Subject: [PATCH 17/89] Me: Fixes logic for gated security checkup nav tab --- client/me/security-section-nav/index.jsx | 49 +++++++++++++----------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/client/me/security-section-nav/index.jsx b/client/me/security-section-nav/index.jsx index 50ccad3d55c0f..7ba335831f6d4 100644 --- a/client/me/security-section-nav/index.jsx +++ b/client/me/security-section-nav/index.jsx @@ -18,27 +18,30 @@ module.exports = React.createClass( { path: React.PropTypes.string.isRequired }, - getDefaultProps: function() { - return { - tabs: [ - { - title: i18n.translate( 'Password', { textOnly: true } ), - path: '/me/security', - }, - { - title: i18n.translate( 'Two-Step Authentication', { textOnly: true } ), - path: '/me/security/two-step', - }, - { - title: i18n.translate( 'Connected Applications', { textOnly: true } ), - path: '/me/security/connected-applications', - }, - config.isEnabled( 'me/security/checkup' ) ? { - title: i18n.translate( 'Checkup', { textOnly: true } ), - path: '/me/security/checkup', - } : false - ] - }; + getNavtabs: function() { + var tabs = [ + { + title: i18n.translate( 'Password', { textOnly: true } ), + path: '/me/security', + }, + { + title: i18n.translate( 'Two-Step Authentication', { textOnly: true } ), + path: '/me/security/two-step', + }, + { + title: i18n.translate( 'Connected Applications', { textOnly: true } ), + path: '/me/security/connected-applications', + } + ]; + + if ( config.isEnabled( 'me/security/checkup' ) ) { + tabs.push( { + title: i18n.translate( 'Checkup', { textOnly: true } ), + path: '/me/security/checkup', + } ); + } + + return tabs; }, getFilteredPath: function() { @@ -48,7 +51,7 @@ module.exports = React.createClass( { getSelectedText: function() { var text = '', - found = find( this.props.tabs, function( tab ) { + found = find( this.getNavtabs(), function( tab ) { return this.getFilteredPath() === tab.path; }, this ); @@ -67,7 +70,7 @@ module.exports = React.createClass( { return ( - { this.props.tabs.map( function( tab ) { + { this.getNavtabs().map( function( tab ) { return ( Date: Mon, 7 Dec 2015 18:20:16 -0600 Subject: [PATCH 18/89] search component: replace noticon with gridicon search open and close icons: replace all other instances of noticon references search: alignment multi author: align --- assets/stylesheets/sections/_menus.scss | 2 +- client/components/author-selector/style.scss | 4 +- client/components/search/index.jsx | 13 ++++--- client/components/search/style.scss | 39 +++++++------------ client/components/site-selector/style.scss | 24 +++++------- client/my-sites/category-selector/search.jsx | 8 +++- client/my-sites/category-selector/search.scss | 2 +- .../menus/item-options/option-list.jsx | 5 ++- client/post-editor/editor-location/style.scss | 10 +++-- 9 files changed, 51 insertions(+), 56 deletions(-) diff --git a/assets/stylesheets/sections/_menus.scss b/assets/stylesheets/sections/_menus.scss index aaa110cfde5d0..67b731f37a10f 100644 --- a/assets/stylesheets/sections/_menus.scss +++ b/assets/stylesheets/sections/_menus.scss @@ -839,7 +839,7 @@ .search-container { position: relative; - .noticon-search { + .gridicon { position: absolute; left: 0; padding: 9px 9px; diff --git a/client/components/author-selector/style.scss b/client/components/author-selector/style.scss index e9e2dddcbeeb3..088fd9fe73620 100644 --- a/client/components/author-selector/style.scss +++ b/client/components/author-selector/style.scss @@ -53,8 +53,8 @@ height: 43px; margin-bottom: 0; - .is-open .noticon-search { - color: $gray; + &.is-open .gridicon { + top: 12px; } .search__input[type="search"] { diff --git a/client/components/search/index.jsx b/client/components/search/index.jsx index 364c7b2dd89d2..37526d1ed67d7 100644 --- a/client/components/search/index.jsx +++ b/client/components/search/index.jsx @@ -10,7 +10,8 @@ var React = require( 'react' ), * Internal dependencies */ var analytics = require( 'analytics' ), - Spinner = require( 'components/spinner' ); + Spinner = require( 'components/spinner' ), + Gridicon = require( 'components/gridicon' ); /** * Internal variables @@ -252,7 +253,6 @@ module.exports = React.createClass( {
+ aria-label={ this.translate( 'Open Search', { context: 'button label' } ) }> + +
+ aria-label={ this.translate( 'Close Search', { context: 'button label' } ) }> + + ); }, diff --git a/client/components/search/style.scss b/client/components/search/style.scss index 385c03b8ac3ea..f212c60b0260f 100644 --- a/client/components/search/style.scss +++ b/client/components/search/style.scss @@ -13,10 +13,9 @@ width: 50px; } - .noticon-search { + .search-open__icon { position: absolute; - top: 0; - bottom: 0; + top: 14px; width: 60px; z-index: 20; color: $blue-wordpress; @@ -26,35 +25,25 @@ outline: dotted 1px $blue-wordpress; } - &::before { - position: absolute; - left: 0; - right: 0; - top: 50%; - margin-top: -12px; - font-size: 24px; - text-align: center; - } - @include breakpoint( "<660px" ) { width: 50px; } } - .noticon-search:hover { - color: $gray-dark; + .search-open__icon:hover { + color: darken( $gray, 30% ); } - .noticon-close-alt { + .search-close__icon { position: absolute; bottom: 0; - top: 0; + top: 14px; right: 0; width: 60px; cursor: pointer; z-index: 20; - color: $gray-dark; + color: darken( $gray, 30% ); display: none; opacity: 0; transition: opacity .2s ease-in; @@ -96,7 +85,7 @@ // matching dropdown-selector z-index: 170; - .noticon-search { + .search-open__icon { right: 0; } @@ -112,7 +101,7 @@ top: 0; padding: 0 50px 0 60px; border: none; - background: #fff; + background: $white; height: 51px; appearance: none; box-sizing: border-box; @@ -139,17 +128,17 @@ margin-right: 0 !important; width: 100%; - .noticon-search { - color: $gray-dark; + .search-open__icon { + color: darken( $gray, 30% ); left: 0; } - .noticon-close-alt { + .search-close__icon { display: inline-block; } .search__input, - .noticon-close-alt { + .search-close__icon{ opacity: 1; } @@ -170,7 +159,7 @@ } } -.search.is-searching .noticon-search { +.search.is-searching .search-open__icon { display: none; } diff --git a/client/components/site-selector/style.scss b/client/components/site-selector/style.scss index 5300bd7988a52..1cd70fe80c198 100644 --- a/client/components/site-selector/style.scss +++ b/client/components/site-selector/style.scss @@ -109,7 +109,7 @@ } } - .noticon-search { + .search-open__icon { background-color: transparent; border-left: none; color: $gray; @@ -117,24 +117,18 @@ width: auto; position: absolute; left: 18px; - top: 7px; - - &::before { - font-size: 16px; - } + top: 16px; + width: 16px; + height: 16px; } &.is-open { - .noticon-close-alt { + .search-close__icon { color: $gray; - height: 36px; - width: 34px; - top: 9px; - right: 9px; - - &:before { - font-size: 12px; - } + top: 16px; + right: 16px; + width: 16px; + height: 16px; } } } diff --git a/client/my-sites/category-selector/search.jsx b/client/my-sites/category-selector/search.jsx index ec8b63f045db1..a854ff090191a 100644 --- a/client/my-sites/category-selector/search.jsx +++ b/client/my-sites/category-selector/search.jsx @@ -3,6 +3,12 @@ */ var React = require( 'react' ); +/** + * Internal dependencies + */ +var Gridicon = require( 'components/gridicon' ); + + module.exports = React.createClass( { displayName: 'CategorySelectorSearch', @@ -14,7 +20,7 @@ module.exports = React.createClass( { render: function() { return (
-
+ -
+ Date: Tue, 8 Dec 2015 13:22:31 -0500 Subject: [PATCH 19/89] Search Icon: I changed the positioning of the search and close icons in the search component so that it is always vertically centered regardless of the height of the wrapping card Search: fixed a regression in the modal site-selector search due to my previous commit which made search vertically centered (assuming a 24px icon) Search: icon is now positioned correctly in every instance I could find, this includes the close icon Search: missed one in the site selector close icon --- client/components/author-selector/style.scss | 4 ---- client/components/search/style.scss | 6 ++++-- client/components/site-selector/style.scss | 4 ++-- client/post-editor/editor-location/style.scss | 2 -- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/client/components/author-selector/style.scss b/client/components/author-selector/style.scss index 088fd9fe73620..6f2d6dea61e64 100644 --- a/client/components/author-selector/style.scss +++ b/client/components/author-selector/style.scss @@ -53,10 +53,6 @@ height: 43px; margin-bottom: 0; - &.is-open .gridicon { - top: 12px; - } - .search__input[type="search"] { border-radius: 5px; font-size: 14px; diff --git a/client/components/search/style.scss b/client/components/search/style.scss index f212c60b0260f..18d3ae782927f 100644 --- a/client/components/search/style.scss +++ b/client/components/search/style.scss @@ -15,7 +15,8 @@ .search-open__icon { position: absolute; - top: 14px; + top: 50%; + margin-top: -12px; width: 60px; z-index: 20; color: $blue-wordpress; @@ -38,8 +39,9 @@ .search-close__icon { position: absolute; bottom: 0; - top: 14px; + top: 50%; right: 0; + margin-top: -12px; width: 60px; cursor: pointer; z-index: 20; diff --git a/client/components/site-selector/style.scss b/client/components/site-selector/style.scss index 1cd70fe80c198..03f9c062afaa9 100644 --- a/client/components/site-selector/style.scss +++ b/client/components/site-selector/style.scss @@ -117,7 +117,7 @@ width: auto; position: absolute; left: 18px; - top: 16px; + margin-top: -8px; width: 16px; height: 16px; } @@ -125,8 +125,8 @@ &.is-open { .search-close__icon { color: $gray; - top: 16px; right: 16px; + margin-top: -8px; width: 16px; height: 16px; } diff --git a/client/post-editor/editor-location/style.scss b/client/post-editor/editor-location/style.scss index d543f0e440fa0..bd4697343c9c3 100644 --- a/client/post-editor/editor-location/style.scss +++ b/client/post-editor/editor-location/style.scss @@ -12,7 +12,6 @@ .editor-location__search .search-open__icon { width: 40px; - top: 9px; } .editor-location__search .search, @@ -26,7 +25,6 @@ .editor-location__search .search-close__icon { width: 40px; - top: 9px; } .editor-location__search-results { From e9d7b0b847c42bc0cd4943dba74019cc90f32f48 Mon Sep 17 00:00:00 2001 From: nobuti Date: Wed, 9 Dec 2015 13:13:38 +0100 Subject: [PATCH 20/89] Included hostname in boot message. --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 9ee067ade5f02..651b030c07171 100644 --- a/index.js +++ b/index.js @@ -13,11 +13,12 @@ var pkg = require( './package.json' ), var start = Date.now(), port = process.env.PORT || 3000, + host = config( 'hostname' ), app = boot(), server, hotReloader; -console.log( '%s booted in %dms - port: %s', pkg.name, ( Date.now() ) - start, port ); +console.log( chalk.yellow('%s booted in %dms - http://%s:%s'), pkg.name, ( Date.now() ) - start, host, port ); console.info( chalk.cyan( '\nGetting bundles ready, hold on...' ) ); server = http.createServer( app ); server.listen( port ); From 0320323d11eaa66c1105bf2a99215ec8d500ac1f Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Wed, 9 Dec 2015 17:23:04 +0100 Subject: [PATCH 21/89] Section header: Vertical align label --- client/components/section-header/style.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/components/section-header/style.scss b/client/components/section-header/style.scss index 58738369b6725..c7616f8db8602 100644 --- a/client/components/section-header/style.scss +++ b/client/components/section-header/style.scss @@ -10,10 +10,13 @@ } .section-header__label { + display: flex; + align-items: center; flex-grow: 1; line-height: 28px; position: relative; + &:before { @include long-content-fade( $color : $white ); } From 969fd8bf83b55d85dd24f6fd418d04a650aa67b2 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Wed, 9 Dec 2015 12:14:40 +0100 Subject: [PATCH 22/89] Framework: Check for gridicon sizes on commit --- bin/gridiconFormatChecker | 42 +++++++++++++++++++++++++++++++++++++++ bin/pre-commit | 9 +++++++++ 2 files changed, 51 insertions(+) create mode 100755 bin/gridiconFormatChecker diff --git a/bin/gridiconFormatChecker b/bin/gridiconFormatChecker new file mode 100755 index 0000000000000..4b7bcfd7031d0 --- /dev/null +++ b/bin/gridiconFormatChecker @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +var fs = require( 'fs' ); +var readline = require( 'readline' ); +var validIconSizes = [ 12, 18, 24, 36, 48, 54, 72 ]; + +var file = process.argv[ 2 ]; + +function checkGridicons( filename ) { + var lineNumber = 0, + result = ''; + readline.createInterface( { + input: fs.createReadStream( filename ), + terminal: false + } ).on( 'line', function( line ) { + var gridIconProps, onlyAttrs, size; + + if ( line.indexOf( '= 0 ) { + gridIconProps = line.split( '' )[ 0 ].split( ' ' ).join( '' ); + if ( onlyAttrs.indexOf( 'size={' ) >= 0 ) { + size = onlyAttrs.split( 'size={' )[ 1 ].split( '}' )[ 0 ]; + if ( !isNaN( size ) && validIconSizes.indexOf( +size ) < 0 ) { + result += '\033[31mNon-standard gridicon size ( ' + size + 'px ) detected in ' + filename + ' line ' + lineNumber + '\n'; + } + } + } + } + lineNumber++; + } ).on( 'close', function() { + if ( result !== '' ) { + console.error( result ); + console.log( '\033[mValid gridiconsizes are ' + validIconSizes.join( 'px, ' ) + 'px\n' ); + process.exit( 1 ); + } else { + process.exit( 0 ); + } + } ); +} + +checkGridicons( file ); diff --git a/bin/pre-commit b/bin/pre-commit index e1a5531fd9fa9..ae2d0d77f2256 100755 --- a/bin/pre-commit +++ b/bin/pre-commit @@ -38,6 +38,15 @@ done echo "\neslint validation complete\n" +for file in ${files}; do + ./bin/gridiconFormatChecker ${file} + if [ $? -ne 0 ]; then + echo "\033[31mGridicon Format Check Failed: \033[0m${file}\n" + pass=false + fi +done + + if ! $pass; then echo "\033[41mCOMMIT FAILED:\033[0m Your commit contains files that should pass validation tests but do not. Please fix the errors and try again.\n" exit 1 From 1db7041ce7ca60aa69904afafbe8097d27ec1a1a Mon Sep 17 00:00:00 2001 From: Lance Willett Date: Wed, 9 Dec 2015 13:19:10 -0700 Subject: [PATCH 23/89] Notifications: update grammar to be consistent with "your comments" --- client/me/notification-settings/settings-form/locales.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/me/notification-settings/settings-form/locales.js b/client/me/notification-settings/settings-form/locales.js index 4140271700889..8bfe9fb3a6c66 100644 --- a/client/me/notification-settings/settings-form/locales.js +++ b/client/me/notification-settings/settings-form/locales.js @@ -6,7 +6,7 @@ export const streamLabels = { }; export const settingLabels = { - comment_like: () => i18n.translate( 'Likes on my comments' ), + comment_like: () => i18n.translate( 'Likes on your comments' ), comment_reply: () => i18n.translate( 'Replies to your comments' ), new_comment: () => i18n.translate( 'New Comment' ), From a24b1b11213aff8f42c06cf32cbd4a098706334e Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Wed, 9 Dec 2015 12:22:44 -0800 Subject: [PATCH 24/89] Mock Gridicon component. --- client/components/search/test/index.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/components/search/test/index.jsx b/client/components/search/test/index.jsx index d5e667a34df4a..8b3b5225b6dc4 100644 --- a/client/components/search/test/index.jsx +++ b/client/components/search/test/index.jsx @@ -11,9 +11,16 @@ import mockery from 'mockery'; const expect = chai.expect, TestUtils = React.addons.TestUtils; +const EMPTY_COMPONENT = React.createClass( { + render: function() { + return
; + } +} ); + describe( 'Search', function() { beforeEach( function() { mockery.registerMock( 'analytics', {} ); + mockery.registerMock( 'components/gridicon', EMPTY_COMPONENT ); mockery.enable(); mockery.warnOnUnregistered( false ); From 8737168c6016da6b57571fcdcb8c2095bbe46afb Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Wed, 9 Dec 2015 16:57:31 -0800 Subject: [PATCH 25/89] Disable toolbar pin based upon isMobile not isTouch. --- client/components/tinymce/index.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/components/tinymce/index.jsx b/client/components/tinymce/index.jsx index 5cbb6359d2bf7..ff982f824e6cf 100644 --- a/client/components/tinymce/index.jsx +++ b/client/components/tinymce/index.jsx @@ -43,7 +43,6 @@ require( './plugins/calypso-alert/plugin' )(); const formatting = require( 'lib/formatting' ), user = require( 'lib/user' )(), i18n = require( './i18n' ), - hasTouch = require( 'lib/touch-detect' ).hasTouch, viewport = require( 'lib/viewport' ); /** @@ -170,7 +169,7 @@ module.exports = React.createClass( { this.bindEditorEvents(); editor.on( 'SetTextAreaContent', ( event ) => this.setTextAreaContent( event.content ) ); - if ( ! hasTouch() ) { + if ( ! viewport.isMobile() ) { window.addEventListener( 'scroll', this.onScrollPinTools ); editor.once( 'PostRender', this.toggleEditor.bind( this, { autofocus: ! this.props.isNew } ) ); } From 718fde238b400e60e09ccd6f3e663578272fda74 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Thu, 10 Dec 2015 10:23:40 +0100 Subject: [PATCH 26/89] Gridicons: adds 'nonStandard' prop so the precommit checker can skip certain gridicons --- bin/gridiconFormatChecker | 12 +++++++++--- shared/components/gridicon/README.md | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bin/gridiconFormatChecker b/bin/gridiconFormatChecker index 4b7bcfd7031d0..08d4888f6012d 100755 --- a/bin/gridiconFormatChecker +++ b/bin/gridiconFormatChecker @@ -13,16 +13,22 @@ function checkGridicons( filename ) { input: fs.createReadStream( filename ), terminal: false } ).on( 'line', function( line ) { - var gridIconProps, onlyAttrs, size; + var gridIconProps, onlyAttrs, size, isNonStandard = false; if ( line.indexOf( '= 0 ) { gridIconProps = line.split( '' )[ 0 ].split( ' ' ).join( '' ); + isNonStandard = onlyAttrs.indexOf( 'nonStandard' ) >= 0; if ( onlyAttrs.indexOf( 'size={' ) >= 0 ) { size = onlyAttrs.split( 'size={' )[ 1 ].split( '}' )[ 0 ]; - if ( !isNaN( size ) && validIconSizes.indexOf( +size ) < 0 ) { - result += '\033[31mNon-standard gridicon size ( ' + size + 'px ) detected in ' + filename + ' line ' + lineNumber + '\n'; + if ( !isNaN( size ) ) { + if( !isNonStandard && validIconSizes.indexOf( +size ) < 0 ) { + result += '\033[31mNon-standard gridicon size ( ' + size + 'px ) detected in ' + filename + ' line ' + lineNumber + '\n'; + } + if( isNonStandard && validIconSizes.indexOf( +size ) >= 0 ) { + result += '\033[33mStandard size gridicon ( ' + size + 'px ) marked as non-standard... are you sure that is ok? in ' + filename + ' line ' + lineNumber + '\n'; + } } } } diff --git a/shared/components/gridicon/README.md b/shared/components/gridicon/README.md index 1c0f9373d96e4..05b171b91355e 100644 --- a/shared/components/gridicon/README.md +++ b/shared/components/gridicon/README.md @@ -18,3 +18,4 @@ render: function() { * `icon`: String - the icon name. * `size`: Number - (default: 24) set the size of the icon. * `onClick`: Function - (optional) if you need a click callback. +* `nonStandard`: Boolean - (optional) A semantic prop to indicate (to our automatic tools and other developers) that this gridicon is not using one of the standard sizes on purpose. It must be combined with an additional comment explaining why the odd size is necesary. From 0d8a5218879b8bb1248e944cc62c514902be4f94 Mon Sep 17 00:00:00 2001 From: Veselin Nikolov Date: Thu, 10 Dec 2015 13:02:24 +0200 Subject: [PATCH 27/89] Upgrades: fix right alignment in cancel privacy --- client/me/purchases/cancel-private-registration/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/me/purchases/cancel-private-registration/index.jsx b/client/me/purchases/cancel-private-registration/index.jsx index fc18118326408..2efce3ecf7660 100644 --- a/client/me/purchases/cancel-private-registration/index.jsx +++ b/client/me/purchases/cancel-private-registration/index.jsx @@ -144,7 +144,7 @@ const CancelPrivateRegistration = React.createClass( { return ( -
+
{ titles.cancelPrivateRegistration } From e8ae82a1031f4d4e3351db52e8244f1503a16298 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Thu, 10 Dec 2015 16:54:02 +0100 Subject: [PATCH 28/89] Gridicons: Support for multiline & several gridicons in the same file --- bin/gridiconFormatChecker | 63 ++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/bin/gridiconFormatChecker b/bin/gridiconFormatChecker index 08d4888f6012d..0ebdf91098a42 100755 --- a/bin/gridiconFormatChecker +++ b/bin/gridiconFormatChecker @@ -1,28 +1,33 @@ #!/usr/bin/env node var fs = require( 'fs' ); -var readline = require( 'readline' ); var validIconSizes = [ 12, 18, 24, 36, 48, 54, 72 ]; -var file = process.argv[ 2 ]; +var filename = process.argv[ 2 ]; -function checkGridicons( filename ) { - var lineNumber = 0, - result = ''; - readline.createInterface( { - input: fs.createReadStream( filename ), - terminal: false - } ).on( 'line', function( line ) { - var gridIconProps, onlyAttrs, size, isNonStandard = false; +fs.readFile( filename, 'utf8', function ( err, data ) { + var result = '', + splittedCode, + lineNumber = 1; + if ( err ) { + console.log(err); + process.exit( 1 ); + } + data = data.toLowerCase(); + splittedCode = data.split( '= 0 ) { - gridIconProps = line.split( '' )[ 0 ].split( ' ' ).join( '' ); - isNonStandard = onlyAttrs.indexOf( 'nonStandard' ) >= 0; - if ( onlyAttrs.indexOf( 'size={' ) >= 0 ) { - size = onlyAttrs.split( 'size={' )[ 1 ].split( '}' )[ 0 ]; + if ( splittedCode.length > 1 ) { + // There are gridicon instances in this file. + splittedCode.forEach( function( chunk ) { + var gridiconAttrs, isNonStandard, size; + if( chunk ) { + // we discard all the code after the tag closing... we are only interested in the props of the gridicon. + gridiconAttrs = chunk.split( '>' )[ 0 ]; + isNonStandard = gridiconAttrs.indexOf( 'nonstandard' ) >= 0; + if ( gridiconAttrs.indexOf( 'size={' ) >= 0 ) { + size = gridiconAttrs.split( 'size={' )[ 1 ].split( '}' )[ 0 ]; if ( !isNaN( size ) ) { + // We only can check if the size is standard if it is a number. If not (variables), we have no way of knowing if it's fine or not if( !isNonStandard && validIconSizes.indexOf( +size ) < 0 ) { result += '\033[31mNon-standard gridicon size ( ' + size + 'px ) detected in ' + filename + ' line ' + lineNumber + '\n'; } @@ -31,18 +36,16 @@ function checkGridicons( filename ) { } } } + lineNumber += chunk.split('\n').length - 1; } - } - lineNumber++; - } ).on( 'close', function() { - if ( result !== '' ) { - console.error( result ); - console.log( '\033[mValid gridiconsizes are ' + validIconSizes.join( 'px, ' ) + 'px\n' ); - process.exit( 1 ); - } else { - process.exit( 0 ); - } - } ); -} + } ); + } -checkGridicons( file ); + if ( result !== '' ) { + console.error( result ); + console.log( '\033[m=== Valid gridiconsizes are ' + validIconSizes.join( 'px, ' ) + 'px ===\n' ); + process.exit( 1 ); + } else { + process.exit( 0 ); + } +} ); From d26554b75a9c1ebb1f3d1a3f43bfa19e371b1f9b Mon Sep 17 00:00:00 2001 From: kellychoffman Date: Thu, 10 Dec 2015 10:54:21 -0600 Subject: [PATCH 29/89] 16px gridicons: update to use 18px for clarity --- assets/stylesheets/sections/_menus.scss | 2 +- client/components/site-selector/style.scss | 13 +++++++------ client/my-sites/category-selector/search.jsx | 2 +- client/my-sites/category-selector/search.scss | 4 ++-- client/my-sites/menus/item-options/option-list.jsx | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/assets/stylesheets/sections/_menus.scss b/assets/stylesheets/sections/_menus.scss index 67b731f37a10f..adb74a1c22b6c 100644 --- a/assets/stylesheets/sections/_menus.scss +++ b/assets/stylesheets/sections/_menus.scss @@ -842,7 +842,7 @@ .gridicon { position: absolute; left: 0; - padding: 9px 9px; + padding: 9px 8px; } .search-box { diff --git a/client/components/site-selector/style.scss b/client/components/site-selector/style.scss index 03f9c062afaa9..f9a0ddc37f362 100644 --- a/client/components/site-selector/style.scss +++ b/client/components/site-selector/style.scss @@ -117,18 +117,19 @@ width: auto; position: absolute; left: 18px; - margin-top: -8px; - width: 16px; - height: 16px; + top: 50%; + margin-top: -9px; + width: 18px; + height: 18px; } &.is-open { .search-close__icon { color: $gray; right: 16px; - margin-top: -8px; - width: 16px; - height: 16px; + margin-top: -9px; + width: 18px; + height: 18px; } } } diff --git a/client/my-sites/category-selector/search.jsx b/client/my-sites/category-selector/search.jsx index a854ff090191a..2a4b13f4f6f0c 100644 --- a/client/my-sites/category-selector/search.jsx +++ b/client/my-sites/category-selector/search.jsx @@ -20,7 +20,7 @@ module.exports = React.createClass( { render: function() { return (
- + - + Date: Thu, 10 Dec 2015 09:39:33 -0800 Subject: [PATCH 30/89] Restructure section-nav tests, add mock for gridicon. --- client/components/section-nav/test/index.jsx | 137 +++++++++++-------- 1 file changed, 78 insertions(+), 59 deletions(-) diff --git a/client/components/section-nav/test/index.jsx b/client/components/section-nav/test/index.jsx index 1c135f19fe784..53fc38e374309 100644 --- a/client/components/section-nav/test/index.jsx +++ b/client/components/section-nav/test/index.jsx @@ -1,14 +1,19 @@ var assert = require( 'chai' ).assert, sinon = require( 'sinon' ), React = require( 'react/addons' ), + mockery = require( 'mockery' ), TestUtils = React.addons.TestUtils, SectionNav; +var EMPTY_COMPONENT = React.createClass( { + render: function() { + return
; + } +} ); + require( 'lib/react-test-env-setup' )( '
' ); require( 'react-tap-event-plugin' )(); -SectionNav = require( '../' ); - function createComponent( component, props, children ) { var shallowRenderer = React.addons.TestUtils.createRenderer(); shallowRenderer.render( @@ -16,74 +21,88 @@ function createComponent( component, props, children ) { ); return shallowRenderer.getRenderOutput(); } - -describe( 'Section-Nav rendering', function() { +describe( 'section-nav', function() { before( function() { - var selectedText = 'test'; - var children = (

mmyellow

); + mockery.registerMock( 'components/gridicon', EMPTY_COMPONENT ); + mockery.enable(); + mockery.warnOnUnregistered( false ); - this.sectionNav = createComponent( SectionNav, { - selectedText: selectedText - }, children ); + SectionNav = require( '../' ); + } ), - this.panelElem = this.sectionNav.props.children[ 1 ]; - this.headerElem = this.sectionNav.props.children[ 0 ]; - this.headerTextElem = this.headerElem.props.children; - this.text = this.headerTextElem.props.children; - } ); + after( function() { + mockery.deregisterMock( 'components/gridicon' ); + mockery.disable(); + } ), - it( 'should render a header and a panel', function() { - assert.equal( this.headerElem.props.className, 'section-nav__mobile-header' ); - assert.equal( this.panelElem.props.className, 'section-nav__panel' ); - assert.equal( this.headerTextElem.props.className, 'section-nav__mobile-header-text' ); - } ); + describe( 'rendering', function() { + before( function() { + var selectedText = 'test'; + var children = (

mmyellow

); - it( 'should render selectedText within mobile header', function() { - assert.equal( this.text, 'test' ); - } ); + this.sectionNav = createComponent( SectionNav, { + selectedText: selectedText + }, children ); - it( 'should render children', function( done ) { - //React.Children.only should work here but gives an error about not being the only child - React.Children.map( this.panelElem.props.children, function( obj ) { - if ( obj.type === 'p' ) { - assert.equal( obj.props.children, 'mmyellow' ); - done(); - } + this.panelElem = this.sectionNav.props.children[ 1 ]; + this.headerElem = this.sectionNav.props.children[ 0 ]; + this.headerTextElem = this.headerElem.props.children; + this.text = this.headerTextElem.props.children; } ); - } ); -} ); -describe( 'Section-Nav interaction', function() { - it( 'should call onMobileNavPanelOpen function passed as a prop when tapped', function( done ) { - var elem = React.createElement( SectionNav, { - selectedText: 'placeholder', - onMobileNavPanelOpen: function() { - done(); - } - }, (

placeholder

) ); - var tree = TestUtils.renderIntoDocument( elem ); - assert( ! tree.state.mobileOpen ); - TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'section-nav__mobile-header' ) ) ); - assert( tree.state.mobileOpen ); + it( 'should render a header and a panel', function() { + assert.equal( this.headerElem.props.className, 'section-nav__mobile-header' ); + assert.equal( this.panelElem.props.className, 'section-nav__panel' ); + assert.equal( this.headerTextElem.props.className, 'section-nav__mobile-header-text' ); + } ); + + it( 'should render selectedText within mobile header', function() { + assert.equal( this.text, 'test' ); + } ); + + it( 'should render children', function( done ) { + //React.Children.only should work here but gives an error about not being the only child + React.Children.map( this.panelElem.props.children, function( obj ) { + if ( obj.type === 'p' ) { + assert.equal( obj.props.children, 'mmyellow' ); + done(); + } + } ); + } ); } ); - it( 'should call onMobileNavPanelOpen function passed as a prop twice when tapped three times', function( done ) { - var spy = sinon.spy(); - var elem = React.createElement( SectionNav, { - selectedText: 'placeholder', - onMobileNavPanelOpen: spy - }, (

placeholder

) ); - var tree = TestUtils.renderIntoDocument( elem ); + describe( 'interaction', function() { + it( 'should call onMobileNavPanelOpen function passed as a prop when tapped', function( done ) { + var elem = React.createElement( SectionNav, { + selectedText: 'placeholder', + onMobileNavPanelOpen: function() { + done(); + } + }, (

placeholder

) ); + var tree = TestUtils.renderIntoDocument( elem ); + assert( ! tree.state.mobileOpen ); + TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'section-nav__mobile-header' ) ) ); + assert( tree.state.mobileOpen ); + } ); + + it( 'should call onMobileNavPanelOpen function passed as a prop twice when tapped three times', function( done ) { + var spy = sinon.spy(); + var elem = React.createElement( SectionNav, { + selectedText: 'placeholder', + onMobileNavPanelOpen: spy + }, (

placeholder

) ); + var tree = TestUtils.renderIntoDocument( elem ); - assert( ! tree.state.mobileOpen ); - TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'section-nav__mobile-header' ) ) ); - assert( tree.state.mobileOpen ); - TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'section-nav__mobile-header' ) ) ); - assert( ! tree.state.mobileOpen ); - TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'section-nav__mobile-header' ) ) ); - assert( tree.state.mobileOpen ); + assert( ! tree.state.mobileOpen ); + TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'section-nav__mobile-header' ) ) ); + assert( tree.state.mobileOpen ); + TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'section-nav__mobile-header' ) ) ); + assert( ! tree.state.mobileOpen ); + TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'section-nav__mobile-header' ) ) ); + assert( tree.state.mobileOpen ); - assert( spy.calledTwice ); - done(); + assert( spy.calledTwice ); + done(); + } ); } ); } ); From 969c188f2c81a06921160c04a4e5d0f199980d4b Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Fri, 27 Nov 2015 10:29:34 -0800 Subject: [PATCH 31/89] Editor: Disable drafts button when no drafts exist. --- client/post-editor/drafts-button/index.jsx | 38 ++++++++++++++------- client/post-editor/drafts-button/style.scss | 5 +++ client/post-editor/post-editor.jsx | 4 +-- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/client/post-editor/drafts-button/index.jsx b/client/post-editor/drafts-button/index.jsx index 078e4184c90d6..4dd6e71a6a174 100644 --- a/client/post-editor/drafts-button/index.jsx +++ b/client/post-editor/drafts-button/index.jsx @@ -1,31 +1,43 @@ /** * External dependencies */ -var React = require( 'react' ); +import React, { PropTypes } from 'react'; /** * Internal dependencies */ -var config = require( 'config' ), - Count = require( 'components/count' ); +import Count from 'components/count'; -var DraftsButton = React.createClass( { +export default React.createClass( { + displayName: 'EditorDraftsButton', - render: function() { - if ( ! config.isEnabled( 'editor-drafts' ) || ! this.props.site ) { + propTypes: { + site: PropTypes.object, + count: PropTypes.number, + onClick: PropTypes.func + }, + + getDefaultProps() { + return { + count: 0, + onClick: () => {} + }; + }, + + render() { + if ( ! this.props.site ) { return null; } return ( - ); } - } ); - -module.exports = DraftsButton; diff --git a/client/post-editor/drafts-button/style.scss b/client/post-editor/drafts-button/style.scss index 418864d01a00e..18c2b0031f014 100644 --- a/client/post-editor/drafts-button/style.scss +++ b/client/post-editor/drafts-button/style.scss @@ -4,6 +4,11 @@ font-size: 11px; text-transform: uppercase; + &[disabled] { + cursor: not-allowed; + color: lighten( $gray, 10% ); + } + .count { margin-left: 8px; } diff --git a/client/post-editor/post-editor.jsx b/client/post-editor/post-editor.jsx index fdafb571972f9..23e48c800bd5f 100644 --- a/client/post-editor/post-editor.jsx +++ b/client/post-editor/post-editor.jsx @@ -288,7 +288,7 @@ var PostEditor = React.createClass( {
- { config.isEnabled( 'editor-drafts' ) && this.state.showDrafts ? + { this.state.showDrafts ?
- { config.isEnabled( 'editor-drafts' ) && this.state.showDrafts ? + { this.state.showDrafts ? Date: Fri, 27 Nov 2015 10:31:12 -0800 Subject: [PATCH 32/89] Editor: Remove editor-drafts feature flag. --- config/desktop-mac-app-store.json | 1 - config/desktop.json | 1 - config/development.json | 1 - config/horizon.json | 1 - config/production.json | 1 - config/stage.json | 1 - config/wpcalypso.json | 1 - 7 files changed, 7 deletions(-) diff --git a/config/desktop-mac-app-store.json b/config/desktop-mac-app-store.json index c0483252fa7b1..ebd92f75dad8f 100644 --- a/config/desktop-mac-app-store.json +++ b/config/desktop-mac-app-store.json @@ -25,7 +25,6 @@ "post-editor/iframe-preview": true, "post-editor/live-image-updates": true, "post-editor/pages": true, - "editor-drafts": true, "manage/ads": true, "manage/stats": true, diff --git a/config/desktop.json b/config/desktop.json index 6f50801c3e5b4..6379e9715c8a7 100644 --- a/config/desktop.json +++ b/config/desktop.json @@ -21,7 +21,6 @@ "ad-tracking": true, "community-translator": true, - "editor-drafts": true, "help": false, "mailing-lists/unsubscribe": true, "manage/ads": true, diff --git a/config/development.json b/config/development.json index de4ddaa6b744d..4c1d93f21bd89 100644 --- a/config/development.json +++ b/config/development.json @@ -43,7 +43,6 @@ "post-editor/pages": true, "post-editor-github-link": false, "post-editor/post-type-switch": false, - "editor-drafts": true, "manage/media": true, "manage/posts": true, diff --git a/config/horizon.json b/config/horizon.json index cdb7ca823dc5b..636ad9dc1123b 100644 --- a/config/horizon.json +++ b/config/horizon.json @@ -18,7 +18,6 @@ "post-editor/iframe-preview": true, "post-editor/live-image-updates": true, "post-editor/pages": true, - "editor-drafts": true, "manage/ads": true, "manage/ads/jetpack": true, diff --git a/config/production.json b/config/production.json index 78d7340d01e92..739670c2696e7 100644 --- a/config/production.json +++ b/config/production.json @@ -53,7 +53,6 @@ "post-editor/iframe-preview": true, "post-editor/live-image-updates": true, "post-editor/pages": true, - "editor-drafts": true, "help": false, "upgrades/checkout": true, "upgrades/domain-management/contacts-privacy": true, diff --git a/config/stage.json b/config/stage.json index a1a2eb35eb00c..e3154ca81774b 100644 --- a/config/stage.json +++ b/config/stage.json @@ -17,7 +17,6 @@ "post-editor/iframe-preview": true, "post-editor/live-image-updates": true, "post-editor/pages": true, - "editor-drafts": true, "manage/ads": true, "manage/ads/jetpack": true, "manage/customize": true, diff --git a/config/wpcalypso.json b/config/wpcalypso.json index e2b4a30847d70..61e12f4e7ebe6 100644 --- a/config/wpcalypso.json +++ b/config/wpcalypso.json @@ -18,7 +18,6 @@ "post-editor/iframe-preview": true, "post-editor/live-image-updates": true, "post-editor/pages": true, - "editor-drafts": true, "manage/ads": true, "manage/ads/jetpack": true, From d0ab116c1703c3b4fcc57bb638531ee8f7ad47dd Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Wed, 2 Dec 2015 07:53:30 -0800 Subject: [PATCH 33/89] Add additional check to ensure draft count > 0 --- client/post-editor/drafts-button/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/post-editor/drafts-button/index.jsx b/client/post-editor/drafts-button/index.jsx index 4dd6e71a6a174..5bd11613767fa 100644 --- a/client/post-editor/drafts-button/index.jsx +++ b/client/post-editor/drafts-button/index.jsx @@ -36,7 +36,7 @@ export default React.createClass( { aria-label={ this.translate( 'View all drafts' ) } > { this.translate( 'Drafts' ) } - { this.props.count && } + { this.props.count && this.props.count > 0 && } ); } From 6bd64026e2160aa22b0b7462b0073a3bd0b0108e Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Tue, 8 Dec 2015 07:31:30 -0800 Subject: [PATCH 34/89] Revert to old ternary operator. --- client/post-editor/drafts-button/index.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/post-editor/drafts-button/index.jsx b/client/post-editor/drafts-button/index.jsx index 5bd11613767fa..ab81406df7ace 100644 --- a/client/post-editor/drafts-button/index.jsx +++ b/client/post-editor/drafts-button/index.jsx @@ -36,7 +36,9 @@ export default React.createClass( { aria-label={ this.translate( 'View all drafts' ) } > { this.translate( 'Drafts' ) } - { this.props.count && this.props.count > 0 && } + { this.props.count ? + + : null } ); } From c1a250062915ff6c398bf6e0b1aa8f96df4c0bea Mon Sep 17 00:00:00 2001 From: jancavan Date: Tue, 8 Dec 2015 10:50:08 -0800 Subject: [PATCH 35/89] Stats: fix all time zero colors --- client/my-sites/stats/all-time/index.jsx | 15 +++++++++++---- client/my-sites/stats/all-time/style.scss | 4 ++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/my-sites/stats/all-time/index.jsx b/client/my-sites/stats/all-time/index.jsx index f91fbc9f29f9b..f9643e37a273c 100644 --- a/client/my-sites/stats/all-time/index.jsx +++ b/client/my-sites/stats/all-time/index.jsx @@ -30,10 +30,15 @@ module.exports = React.createClass( { return String.fromCharCode( 8211 ); }, + isLow: function( value ) { + return ! value || 0 === value + }, + render: function() { var bestDay = null, infoIcon = this.state.showInfo ? 'info' : 'info-outline', valueClass, + bestViews, classes; if ( this.props.allTimeList.response['best-views'] && this.props.allTimeList.response['best-views'].day ) { @@ -55,6 +60,8 @@ module.exports = React.createClass( { } ]; + bestViews = this.props.allTimeList.response['best-views'] ? this.props.allTimeList.response['best-views'].count : null; + return (
@@ -94,22 +101,22 @@ module.exports = React.createClass( {
  • { this.translate( 'Posts' ) } - { this.ensureValue( this.props.allTimeList.response.posts ) } + { this.ensureValue( this.props.allTimeList.response.posts ) }
  • { this.translate( 'Views' ) } - { this.ensureValue( this.props.allTimeList.response.views ) } + { this.ensureValue( this.props.allTimeList.response.views ) }
  • { this.translate( 'Visitors' ) } - { this.ensureValue( this.props.allTimeList.response.visitors ) } + { this.ensureValue( this.props.allTimeList.response.visitors ) }
  • { this.translate( 'Best Views Ever' ) } - { this.ensureValue( this.props.allTimeList.response['best-views'] ? this.props.allTimeList.response['best-views'].count : null ) } + { this.ensureValue( bestViews ) } { bestDay }
  • diff --git a/client/my-sites/stats/all-time/style.scss b/client/my-sites/stats/all-time/style.scss index 13d35be160723..2c8ac6dc54e92 100644 --- a/client/my-sites/stats/all-time/style.scss +++ b/client/my-sites/stats/all-time/style.scss @@ -106,6 +106,10 @@ animation: loading-fade 1.6s ease-in-out infinite; } + &.is-low { + color: $gray; + } + @include breakpoint( "<480px" ) { clear: none; display: inline-table; // fixes "Best" section moving down the next line when resizing screen From 4ec4805bbc39e5f76431ca55a40b1549243e0e82 Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Thu, 10 Dec 2015 12:32:54 -0800 Subject: [PATCH 36/89] Move scrollpintools call outside of isMobile block. --- client/components/tinymce/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/tinymce/index.jsx b/client/components/tinymce/index.jsx index ff982f824e6cf..876498a163a63 100644 --- a/client/components/tinymce/index.jsx +++ b/client/components/tinymce/index.jsx @@ -169,8 +169,8 @@ module.exports = React.createClass( { this.bindEditorEvents(); editor.on( 'SetTextAreaContent', ( event ) => this.setTextAreaContent( event.content ) ); + window.addEventListener( 'scroll', this.onScrollPinTools ); if ( ! viewport.isMobile() ) { - window.addEventListener( 'scroll', this.onScrollPinTools ); editor.once( 'PostRender', this.toggleEditor.bind( this, { autofocus: ! this.props.isNew } ) ); } }.bind( this ); From 9edc979e9b7ff72ea48991a9043bebcf73e4e4d0 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Thu, 10 Dec 2015 22:07:51 +0100 Subject: [PATCH 37/89] Gridicons: change the nonStandard property name to reflect it only makes reference to size --- bin/gridiconFormatChecker | 2 +- shared/components/gridicon/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/gridiconFormatChecker b/bin/gridiconFormatChecker index 0ebdf91098a42..08fc64dd74608 100755 --- a/bin/gridiconFormatChecker +++ b/bin/gridiconFormatChecker @@ -23,7 +23,7 @@ fs.readFile( filename, 'utf8', function ( err, data ) { if( chunk ) { // we discard all the code after the tag closing... we are only interested in the props of the gridicon. gridiconAttrs = chunk.split( '>' )[ 0 ]; - isNonStandard = gridiconAttrs.indexOf( 'nonstandard' ) >= 0; + isNonStandard = gridiconAttrs.indexOf( 'nonstandardsize' ) >= 0; if ( gridiconAttrs.indexOf( 'size={' ) >= 0 ) { size = gridiconAttrs.split( 'size={' )[ 1 ].split( '}' )[ 0 ]; if ( !isNaN( size ) ) { diff --git a/shared/components/gridicon/README.md b/shared/components/gridicon/README.md index 05b171b91355e..18dc947b41662 100644 --- a/shared/components/gridicon/README.md +++ b/shared/components/gridicon/README.md @@ -18,4 +18,4 @@ render: function() { * `icon`: String - the icon name. * `size`: Number - (default: 24) set the size of the icon. * `onClick`: Function - (optional) if you need a click callback. -* `nonStandard`: Boolean - (optional) A semantic prop to indicate (to our automatic tools and other developers) that this gridicon is not using one of the standard sizes on purpose. It must be combined with an additional comment explaining why the odd size is necesary. +* `nonStandardSize`: Boolean - (optional) A semantic prop to indicate (to our automatic tools and other contributors) that this gridicon is not using one of the standard sizes on purpose. It must be combined with an additional comment explaining why the odd size is necesary. From bec72db0e7e6a934b4ff80846397d3c70bd6fa7a Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:09:29 -0500 Subject: [PATCH 38/89] Styles: Fix indentation warnings in assets/stylesheets See #1475 --- .../stylesheets/sections/_domain-search.scss | 14 +-- assets/stylesheets/sections/_posts.scss | 4 +- .../stylesheets/sections/_site-settings.scss | 10 +- assets/stylesheets/sections/_stats.scss | 24 ++--- assets/stylesheets/sections/_translator.scss | 92 +++++++++---------- assets/stylesheets/shared/_extends.scss | 8 +- assets/stylesheets/shared/_livechat.scss | 2 +- 7 files changed, 77 insertions(+), 77 deletions(-) diff --git a/assets/stylesheets/sections/_domain-search.scss b/assets/stylesheets/sections/_domain-search.scss index 422d466a1deeb..ef9298c72eff1 100644 --- a/assets/stylesheets/sections/_domain-search.scss +++ b/assets/stylesheets/sections/_domain-search.scss @@ -180,7 +180,7 @@ form.google-apps-dialog { } .google-apps-dialog__user-fields { - animation: google-apps-user-show 0.3s ease-in-out; + animation: google-apps-user-show 0.3s ease-in-out; margin-bottom: 20px; } @@ -226,13 +226,13 @@ form.google-apps-dialog { } @keyframes "google-apps-user-show" { - 0% { - max-height: 0px; - } + 0% { + max-height: 0px; + } - 100% { - max-height: 150px; - } + 100% { + max-height: 150px; + } } .google-apps-dialog__footer { diff --git a/assets/stylesheets/sections/_posts.scss b/assets/stylesheets/sections/_posts.scss index 26818a6a9569d..bb2160c9d6022 100644 --- a/assets/stylesheets/sections/_posts.scss +++ b/assets/stylesheets/sections/_posts.scss @@ -332,11 +332,11 @@ /* RTL */ .rtl .posts__list { - //smaller font, let's use Tahoma + //smaller font, let's use Tahoma .post__quote { font-family: $sans-rtl; } - //we can use the default sans for titles + //we can use the default sans for titles .post__title { font-family: $sans; } diff --git a/assets/stylesheets/sections/_site-settings.scss b/assets/stylesheets/sections/_site-settings.scss index a424060de0d81..5cb3c79cfb3c5 100644 --- a/assets/stylesheets/sections/_site-settings.scss +++ b/assets/stylesheets/sections/_site-settings.scss @@ -56,11 +56,11 @@ float: right; } - .empty-content { - .is-primary { - float: none; - } - } + .empty-content { + .is-primary { + float: none; + } + } p.settings-explanation { display: block; diff --git a/assets/stylesheets/sections/_stats.scss b/assets/stylesheets/sections/_stats.scss index d2d82c2efe274..e1084b9880381 100644 --- a/assets/stylesheets/sections/_stats.scss +++ b/assets/stylesheets/sections/_stats.scss @@ -691,18 +691,18 @@ ul.module-tabs { } } - &.is-enabled { - background: $gray-light; + &.is-enabled { + background: $gray-light; - &, - li { + &, + li { border-color: $gray-light; - } + } - a { - background: $gray-light; - } - } + a { + background: $gray-light; + } + } } // Module Content @@ -928,9 +928,9 @@ ul.module-tabs { from { opacity: 1; } - to { - opacity: 0; - } + to { + opacity: 0; + } } .stats-poll__message { diff --git a/assets/stylesheets/sections/_translator.scss b/assets/stylesheets/sections/_translator.scss index 771daac127f58..e1170d7fe8447 100644 --- a/assets/stylesheets/sections/_translator.scss +++ b/assets/stylesheets/sections/_translator.scss @@ -38,64 +38,64 @@ } } - &.active { - background: $white; - a { - color: $blue-wordpress; + &.active { + background: $white; + a { + color: $blue-wordpress; + } } - } } //Overwriting the popup defaults body { - .webui-popover { - border-radius: 2px; - padding: 0; - text-align: inherit; - border-color: lighten( $gray, 20% ); - z-index: 100300; // Appear above dialog + .webui-popover { + border-radius: 2px; + padding: 0; + text-align: inherit; + border-color: lighten( $gray, 20% ); + z-index: 100300; // Appear above dialog - .webui-popover-title { - background-color: lighten( $gray, 20% ); - border-color: lighten( $gray, 30% ); - border-radius: 1px 1px 0 0; - } + .webui-popover-title { + background-color: lighten( $gray, 20% ); + border-color: lighten( $gray, 30% ); + border-radius: 1px 1px 0 0; + } - &.top, - &.top-right, - &.top-left { - .arrow { - border-top-color: lighten( $gray, 20% ); - } - } + &.top, + &.top-right, + &.top-left { + .arrow { + border-top-color: lighten( $gray, 20% ); + } + } - &.right, - &.right-top, - &.right-bottom { - .arrow { - border-right-color: lighten( $gray, 20% ); - } - } + &.right, + &.right-top, + &.right-bottom { + .arrow { + border-right-color: lighten( $gray, 20% ); + } + } - &.left, - &.left-top, - &.left-bottom { - .arrow { - border-left-color: lighten( $gray, 20% ); - } - } + &.left, + &.left-top, + &.left-bottom { + .arrow { + border-left-color: lighten( $gray, 20% ); + } + } - &.bottom, - &.bottom-right, - &.bottom-left { - .arrow { - border-bottom-color: lighten( $gray, 20% ); - &:after { - border-bottom-color: lighten( $gray, 20% ); + &.bottom, + &.bottom-right, + &.bottom-left { + .arrow { + border-bottom-color: lighten( $gray, 20% ); + &:after { + border-bottom-color: lighten( $gray, 20% ); + } + } } - } } - } } .translator-modal { diff --git a/assets/stylesheets/shared/_extends.scss b/assets/stylesheets/shared/_extends.scss index 6fe475147c5a7..a52abf8bc84d1 100644 --- a/assets/stylesheets/shared/_extends.scss +++ b/assets/stylesheets/shared/_extends.scss @@ -112,10 +112,10 @@ %mobile-link-element { -webkit-tap-highlight-color: rgba($white, .4); // Until we capture ontouch events in JS this is better than :active - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } %mobile-interface-element { diff --git a/assets/stylesheets/shared/_livechat.scss b/assets/stylesheets/shared/_livechat.scss index 4de6ddd9e6caf..7d97bc5a03a8d 100644 --- a/assets/stylesheets/shared/_livechat.scss +++ b/assets/stylesheets/shared/_livechat.scss @@ -1004,5 +1004,5 @@ div.hbl_pal_main_width { } .olrk-state-compressed div.hbl_pal_main_width { - width: 28px !important; + width: 28px !important; } From 422c5855d52c15c1ecf026390637bc7d798aa072 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:11:30 -0500 Subject: [PATCH 39/89] Styles: Fix indentation warnings in Gridicon component --- shared/components/gridicon/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/components/gridicon/style.scss b/shared/components/gridicon/style.scss index 44765ab673834..dadf1dffd3462 100644 --- a/shared/components/gridicon/style.scss +++ b/shared/components/gridicon/style.scss @@ -2,6 +2,6 @@ fill: currentColor; &.needs-offset{ - transform: translate( .5px, .5px ); + transform: translate( .5px, .5px ); } } From 2cee32d6be33016cb52793f08b0feb7b33d04013 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:12:38 -0500 Subject: [PATCH 40/89] Styles: Fix indentation warnings on previous-step-button component --- client/signup/previous-step-button/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/signup/previous-step-button/style.scss b/client/signup/previous-step-button/style.scss index 014f66b7859cd..d1f709cb15280 100644 --- a/client/signup/previous-step-button/style.scss +++ b/client/signup/previous-step-button/style.scss @@ -8,7 +8,7 @@ .previous-step__label { color: darken( $gray, 20% ); font-size: 11px; - font-weight: 600; + font-weight: 600; text-transform: uppercase; &:before { @@ -25,4 +25,4 @@ .rtl .previous-step__label:before { margin-top: -3px; transform: rotate( 90deg ); -} \ No newline at end of file +} From bb8e5cb5b58d3e0967e2e3e4c8419fec2ff886be Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Mon, 30 Nov 2015 19:13:35 -0600 Subject: [PATCH 41/89] Adds a new action-only poller This polling component can wrap another component in a polling mechanism that runs a given function at a given interval and will optionally stop polling if the component enters a hidden visibility state to save on bandwidth. If specified to pause when hidden, there is an optional parameter describing whether or not the action should fire immediately upon coming back into view. If not, it will fire after the given interval. This component was inspired by the data-poller component but attempts to be a more lightweight and readable component. Fix poller to work When I wrote the first draft of this component, I was doing so on a plane without Internet access and I did not test it out. Not surprisingly, it didn't work. This revision updates it to work the way the original code was intended. This does not change the functionality of the code, merely the syntax, as the way I thought I might be able to apply the fat-arrow function syntax didn't work in practice. Renamed ActionPoller -> Interval Refactor the interval to aggregate actions This is a significant change from my earlier proposal. In this iteration, we use a stateful backing called `runner.js` which both tracks which actions need to be executed and when. It coalesces operations so that every action that needs to occur, for example, every minute, does so together every minute instead of being scattered about. Currently there are only a few intervals available _by design_ to keep things simple and encourage aggregation. Further interval groups should be added only with a proper reason why the existing set is insufficient. The React component in this iteration is mostly an interface to the underlying `runner.js`, but handles all of the visibility issues. Further, the `leading` property has been removed because this interval makes no guarantees about when exactly that the callbacks will execute, only that they will do so on the specified interval. Probably the default behavior ought to be that the callbacks get called initially after being added to the list. fixup! Refactor the interval to aggregate actions fixup! Refactor the interval to aggregate actions Interval: Ease on the return for Immutable.Map.forEach When I wrote the code for the `forEach` handler I noticed a warning in the documentation for Immutable.js that said any iteration returning `false` would stop iteration. Because of this, I explicitly returned true to make sure we went through the whole list. Today I had a chance to test out the behavior and figure out exactly what happens and so I have rewritten the code to make a better use of this rule. - If the function inside of `forEach` returns === `false` then it will stop iteration. `undefined` and `null` do not terminate iteration. - Because we still pass of execution to `onTick()` which understandably could return `false`, I'm leaving in a guard around that call. Interval: refactor tests to obviate `Interval.get()` Previously the tests introspected the interval state object to make sure that the appropriate behaviors were reflected in the data structure. Practically speaking, however, the tests should be more focussed on the result of those behaviors, not on how the code internally organizes them. The tests have thus been improved by decoupling from the library code itself through the use of mutable callbacks that announce whether or not they have run by modifying an object in the test runners. See `nudgeObject()` Further, the fake timers from **Sinon** have been used to help eliminate these dependencies. fixup! Interval: refactor tests to obviate `Interval.get()` Interval: Update documentation Interval: Pass props to children, update README, test `` Previously the React component part of this feature was untested. In this commit it is properly tested. To help with testing, the new `enzyme` library is pulled in, making the React testing significantly easier. The README has been updated. Interval: Move time period definitions into runner.js As a solution to defining these values in two places I have moved them into `runner.js`. Initially I tried to keep them in `index.js`, but because there would have created a circular dependency I placed them back in `runner.js`. Of note, I think that I should have been able to re-export them at the top of `index.js` without having a separate import/export block, but this left the code without their definitions, so I had to import first, then export. Additionally, the re-export syntax here isn't technically the re-export syntax. ```js // re-exporting export { EVERY_SECOND } from './runner'; ``` The reason I left out the `from './runner'` is because it's not needed here and I wasn't sure what kinds of implications it would bring. ES6 officially supports the re-exporting and syntax I was to use, but Babel actually transpiles this all back to some form of CommonJS module, making the actual output a bit hazy to me. For now, however, we have the definitions in a single place, most-closely tied to core that _should_ define them, and all the tests pass. Interval: Fix typo in README.md Interval: Use optional arg in `setTimeout` instead of creating closure Previously we were creating a closure to get the argument into `executePeriodActions` in the `setTimeout` call but this has been changed because `setTimeout` accepts an optional sequence of arguments to pass to the callback. This is favorable because it's less resource-heavy. Interval: Add clarifying comment about browser visibility Interval: No need to return unused value in `storeNewAction` Interval: Add documentation to runner.js Interval: Return `null` when no children present. Previously if there were no children the Interval would return `` instead of `null`. Now it returns `null` To accomodate this the test suite had to be updated because of what appear to be issues with the library handling `null` values. For now, those instances have been changed to include an empty `
    ` just for the tests themselves. --- client/lib/interval/Makefile | 11 ++ client/lib/interval/README.md | 43 ++++++++ client/lib/interval/index.js | 98 +++++++++++++++++ client/lib/interval/runner.js | 149 +++++++++++++++++++++++++ client/lib/interval/test/component.js | 117 ++++++++++++++++++++ client/lib/interval/test/runner.js | 151 ++++++++++++++++++++++++++ package.json | 1 + 7 files changed, 570 insertions(+) create mode 100644 client/lib/interval/Makefile create mode 100644 client/lib/interval/README.md create mode 100644 client/lib/interval/index.js create mode 100644 client/lib/interval/runner.js create mode 100644 client/lib/interval/test/component.js create mode 100644 client/lib/interval/test/runner.js diff --git a/client/lib/interval/Makefile b/client/lib/interval/Makefile new file mode 100644 index 0000000000000..472771474c271 --- /dev/null +++ b/client/lib/interval/Makefile @@ -0,0 +1,11 @@ +REPORTER ?= spec +NODE_BIN := $(shell npm bin) +MOCHA ?= $(NODE_BIN)/mocha +BASE_DIR := $(NODE_BIN)/../.. +NODE_PATH := $(BASE_DIR)/client:$(BASE_DIR)/shared + +test: + @NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers js:babel/register --reporter $(REPORTER) --ui bdd + +.PHONY: test + diff --git a/client/lib/interval/README.md b/client/lib/interval/README.md new file mode 100644 index 0000000000000..3e6148c993d18 --- /dev/null +++ b/client/lib/interval/README.md @@ -0,0 +1,43 @@ +# Interval + +An interface into a global timer coalescing interval runner. + +_**An interface**_ because this component handles registering and un-registering the given `onTick` action with the global runner. + +_**timer coalescing**_ because the global runner will run at the same time all of the actions registered for a given interval period. For example, if four actions are registered for running once per minute, they will all run at one point in time every minute instead of having four different run times, each time repeating every minute. This is better for things like battery life because it allows for longer and deeper periods of sleep between actions. + +_**interval runner**_ because the global runner will execute the given action every interval on the interval. + +This component can be used to easily trigger a polling, looping, or interval action in the background. Such usages could include polling an API endpoint for updates, sweeping over user input such as in the post editor at intervals, or updating an on-screen timer. + +It only requires two inputs: an action to perform and an interval between which runs of the action should occur. The action is simply a function and the interval is a named constant representing the interval period. These interval periods are intentionally limited to prevent sprawl of timers. + +The action will only be executed as long as the React component is mounted, as the component un-registers the action on unmount. Additionally, the default behavior is to stop executing the action when the browser document is hidden, though this can be overwritten. "Hidden" means that another browser tab is selected or the browser is minimized. + +Wrapped components will be transferred all additional props not consumed by the `` itself. + +## Usage + +```jsx +import Interval, { EVERY_FIVE_SECONDS, EVERY_MINUTE } from 'lib/interval'; + + + +// Wrapping a component + + + + +// Wrapping passes down props +const CounterDisplay = counter =>
    { counter }
    ; + + + + +``` + +## Props + + - `onTick`: Function to run on interval, _required_ + - `period`: Constant specifying interval period, _required_ + - `pauseWhenHidden`: _[true]_ Boolean indicating whether or not to stop executing the action when the browser document is hidden from view. \ No newline at end of file diff --git a/client/lib/interval/index.js b/client/lib/interval/index.js new file mode 100644 index 0000000000000..01a53f89f00dd --- /dev/null +++ b/client/lib/interval/index.js @@ -0,0 +1,98 @@ +import React, { PropTypes } from 'react'; +import omit from 'lodash/object/omit'; + +import { + add, remove, + EVERY_SECOND, + EVERY_FIVE_SECONDS, + EVERY_TEN_SECONDS, + EVERY_THIRTY_SECONDS, + EVERY_MINUTE +} from './runner'; + +export { + EVERY_SECOND, + EVERY_FIVE_SECONDS, + EVERY_TEN_SECONDS, + EVERY_THIRTY_SECONDS, + EVERY_MINUTE +}; + +/** + * Calls a given function on a given interval + */ +export default React.createClass( { + displayName: 'Interval', + + propTypes: { + onTick: PropTypes.func.isRequired, + period: PropTypes.oneOf( [ + EVERY_SECOND, + EVERY_FIVE_SECONDS, + EVERY_TEN_SECONDS, + EVERY_THIRTY_SECONDS, + EVERY_MINUTE + ] ).isRequired, + pauseWhenHidden: PropTypes.bool, + children: PropTypes.element + }, + + getDefaultProps: () => ( { + pauseWhenHidden: true + } ), + + getInitialState: () => ( { + id: null + } ), + + componentDidMount() { + this.start(); + + document.addEventListener( 'visibilitychange', this.handleVisibilityChange, false ); + }, + + componentWillUnmount() { + document.removeEventListener( 'visibilitychange', this.handleVisibilityChange, false ); + + this.stop(); + }, + + componentDidUpdate( prevProps ) { + if ( prevProps.period === this.props.period && prevProps.onTick === this.props.onTick ) { + return; + } + + this.start(); + }, + + handleVisibilityChange() { + const { id } = this.state; + const { pauseWhenHidden } = this.props; + + if ( document.hidden && id && pauseWhenHidden ) { + return this.stop(); + } + + if ( ! document.hidden && ! id && pauseWhenHidden ) { + this.start(); + } + }, + + start() { + const { period, onTick } = this.props; + + if ( this.state.id ) { + remove( this.state.id ); + } + this.setState( { id: add( period, onTick ) } ); + }, + + stop() { + remove( this.state.id ); + this.setState( { id: null } ); + }, + + render() { + return this.props.children ? React.cloneElement( this.props.children, omit( this.props, [ 'onTick', 'period', 'pauseWhenHidden', 'children' ] ) ) : null; + } +} ); diff --git a/client/lib/interval/runner.js b/client/lib/interval/runner.js new file mode 100644 index 0000000000000..fedf10f56dfd1 --- /dev/null +++ b/client/lib/interval/runner.js @@ -0,0 +1,149 @@ +/** + * Global interval action runner + * + * This module contains both a store for keeping track of + * actions that need to run at intervals and the code used + * to execute those actions. + * + * Note: this is not a Flux or a Redux model and the store + * of actions here isn't intended to be exported higher up + * in the application. This module is a singleton that should + * work concurrently for multiple callers from the `` + * component. + * + * # Basic operation + * + * The store keeps track of actions as they are added and removed. + * Every time an action is added to the store a unique id is + * returned much in the same way as with `setTimeout`. This id + * can be used to reference that particular action for later + * removal. + * + * const id = add( EVERY_SECOND, doSomething ); + * remove( id ); + * + * Instead of employing any long-running process to manage + * executing the actions, we instead guarantee that a timeout gets + * set whenever a new action is added. If there are no actions + * stored for a given interval, that timeout gets cleared to make + * sure we don't run for that period. + * + * The scheduling logic all takes place in `scheduleNextRun()` + * which is a safe function to call at any time, meaning that it + * won't overlap timers or break things if we called it needlessly. + */ + +import { fromJS } from 'immutable'; + +export const EVERY_SECOND = 1000; +export const EVERY_FIVE_SECONDS = 5 * 1000; +export const EVERY_TEN_SECONDS = 10 * 1000; +export const EVERY_THIRTY_SECONDS = 30 * 1000; +export const EVERY_MINUTE = 60 * 1000; + +const initialState = fromJS( { + nextId: 1, + periodTimers: { + EVERY_SECOND: null, + EVERY_FIVE_SECONDS: null, + EVERY_TEN_SECONDS: null, + EVERY_THIRTY_SECONDS: null, + EVERY_MINUTE: null + }, + actions: [] +} ); +let state = initialState; + +const increment = a => a + 1; +const addToList = item => list => list.push( item ); +const removeFromList = id => list => list.filterNot( o => o.get( 'id' ) === id ); + +/** + * Resets action store and clears timers + * + * Please don't use in production. This is only + * intended to help with testing code. + */ +export const resetForTesting = () => { + state + .get( 'periodTimers' ) + .forEach( clearTimeout ); + + state = initialState; +}; + +/** + * Adds an action to the queue on the given interval period + * + * @param period one of the constants defining interval periods + * @param {function} onTick the action to run + * @returns {number} unique identifier to use to remove action + */ +export function add( period, onTick ) { + const id = state.get( 'nextId' ); + + storeNewAction( { id, period, onTick } ); + scheduleNextRun(); + + return id; +} + +function storeNewAction( { id, period, onTick } ) { + state = state + .update( 'actions', addToList( fromJS( { id, period, onTick } ) ) ) + .update( 'nextId', increment ); +} + +/** + * Removes an action from the queue + * + * @see add + * + * @param {number} id identifier returned by add() + */ +export function remove( id ) { + removeFromQueue( id ); + scheduleNextRun(); +} + +function removeFromQueue( id ) { + state = state.update( 'actions', removeFromList( id ) ); +} + +function getPeriodActions( period ) { + return state + .get( 'actions' ) + .filter( a => a.get( 'period' ) === period ); +} + +function hasPeriodActions( period ) { + return state + .get( 'actions' ) + .some( a => a.get( 'period' ) === period ); +} + +function executePeriodActions( period ) { + // Make sure we don't return `false` or it will + // halt the iteration in `forEach` + const callAction = a => a.get( 'onTick' ).call() || true; + + getPeriodActions( period ).forEach( callAction ); + + state = state.setIn( [ 'periodTimers', period ], null ); + scheduleNextRun(); +} + +function scheduleNextRun() { + [ EVERY_SECOND, EVERY_FIVE_SECONDS, EVERY_TEN_SECONDS, EVERY_THIRTY_SECONDS, EVERY_MINUTE ] + .forEach( p => { + if ( ! hasPeriodActions( p ) ) { + state = state.updateIn( [ 'periodTimers', p ], clearTimeout ); + return; + } + + if ( ! state.get( 'periodTimers' ).get( p ) ) { + // Note that the second `p` in the call here gets passed as an arg to `executePeriodActions` + state = state.setIn( [ 'periodTimers', p ], setTimeout( executePeriodActions, p, p ) ); + } + } ); +} diff --git a/client/lib/interval/test/component.js b/client/lib/interval/test/component.js new file mode 100644 index 0000000000000..de48d99c3f7b9 --- /dev/null +++ b/client/lib/interval/test/component.js @@ -0,0 +1,117 @@ +require( 'lib/react-test-env-setup' )(); + +/** + * External dependencies + */ +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import React from 'react'; +import { mount, shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import Interval, { EVERY_SECOND, EVERY_MINUTE } from '../index'; +import { add, resetForTesting as reset } from '../runner'; + +const noop = () => null; +const nudgeObject = ( o, v ) => () => ( o.counter += v ); + +describe( 'Interval', function() { + before( function() { + this.clock = sinon.useFakeTimers(); + } ); + + after( function() { + this.clock.restore(); + } ); + + describe( 'Rendering and children', function() { + it( 'Should render an empty span with no children', function() { + const wrapper = shallow( ); + + assert( '' === wrapper.text() ); + } ); + + it( 'Should render children', function() { + const wrapper = shallow(
    test
    ); + + assert( wrapper.contains(
    test
    ) ); + } ); + + it( 'Should pass props to children', function() { + const PropConsumer = React.createClass( { render() { return
    { this.props.prop }
    ; } } ); + const wrapper = shallow( ); + + assert( 42 === wrapper.find( PropConsumer ).prop( 'prop' ) ); + } ); + } ); + + describe( 'Running actions', function() { + beforeEach( function() { + reset(); + } ); + + it( 'Should add the given action', function() { + const id = add( EVERY_MINUTE, noop ); + mount(
    ); + + // verifies that the nextId incremented, signalling that the action was added + assert( id + 2 === add( EVERY_MINUTE, noop ) ); + } ); + + it( 'Runs onTick()', function() { + const o = { counter: 0 }; + mount(
    ); + + assert( 0 === o.counter ); + + this.clock.tick( 1000 ); + assert( 1 === o.counter ); + } ); + + it( 'Changes the callback on prop changes', function() { + const o = { counter: 0 }; + const wrapper = mount(
    ); + + this.clock.tick( 1000 ); + wrapper.setProps( { period: EVERY_MINUTE } ); + + this.clock.tick( 1000 ); + assert( 1 === o.counter ); + + this.clock.tick( 1000 * 60 ); + assert( 2 === o.counter ); + + wrapper.setProps( { onTick: noop } ); + this.clock.tick( 1000 * 60 ); + assert( 2 === o.counter ); + } ); + + it( 'Adds the action when mounted', function() { + const o = { counter: 0 }; + const wrapper = mount(
    ); + + this.clock.tick( 1000 ); + assert( 0 === o.counter ); + + wrapper.setProps( { children:
    } ); + + this.clock.tick( 1000 ); + assert( 1 === o.counter ); + } ); + + it( 'Removes the action when unMounted', function() { + const o = { counter: 0 }; + const wrapper = mount(
    ); + + this.clock.tick( 1000 ); + assert( 1 === o.counter ); + + wrapper.setProps( { children: null } ); + + this.clock.tick( 1000 ); + assert( 1 === o.counter ); + } ); + } ); +} ); diff --git a/client/lib/interval/test/runner.js b/client/lib/interval/test/runner.js new file mode 100644 index 0000000000000..2e0c848a96034 --- /dev/null +++ b/client/lib/interval/test/runner.js @@ -0,0 +1,151 @@ +import { assert } from 'chai'; +import * as sinon from 'sinon'; + +import { + add, remove, + resetForTesting as reset, + EVERY_SECOND, + EVERY_FIVE_SECONDS, + EVERY_TEN_SECONDS, + EVERY_THIRTY_SECONDS, + EVERY_MINUTE +} from '../runner'; + +const noop = () => null; +const nudgeObject = ( o, value ) => () => ( o.counter += value ); + +describe( 'Interval Runner', function() { + before( function() { + this.clock = sinon.useFakeTimers(); + } ); + + after( function() { + this.clock.restore(); + } ); + + beforeEach( function() { + reset(); + } ); + + describe( 'Adding actions', function() { + it( 'Should return the appropriate id', function() { + const o = { counter: 0 }; + const id = add( EVERY_SECOND, nudgeObject( o, 42 ) ); + + assert( 1 === id ); + assert( 0 === o.counter ); + + this.clock.tick( 1000 ); + assert( 42 === o.counter ); + } ); + + it( 'Should increment the next id after adding an action', function() { + [1, 2, 3, 4, 5, 6, 7, 8, 9].forEach( () => add( EVERY_SECOND, noop ) ); + + assert( 10 === add( EVERY_FIVE_SECONDS, noop ) ); + } ); + + it( 'Should add an action to the proper slot', function() { + const o = { counter: 0 }; + add( EVERY_TEN_SECONDS, nudgeObject( o, 42 ) ); + + // plus 1 second + this.clock.tick( 1000 ); + assert( 0 === o.counter ); + + // plus 5 seconds + this.clock.tick( 1000 * 4 ); + assert( 0 === o.counter ); + + // plus 10 seconds + this.clock.tick( 1000 * 5 ); + assert( 42 === o.counter ); + } ); + + it( 'Should add two actions of the same interval to the same slot', function() { + const o = { counter: 0 }; + + add( EVERY_TEN_SECONDS, nudgeObject( o, 3 ) ); + add( EVERY_FIVE_SECONDS, nudgeObject( o, 5 ) ); + add( EVERY_TEN_SECONDS, nudgeObject( o, 7 ) ); + + assert( 0 === o.counter ); + + // plus 5 seconds + this.clock.tick( 1000 * 5 ); + assert( 5 === o.counter ); + + // plus 10 seconds + this.clock.tick( 1000 * 5 ); + assert( 5 + 5 + 3 + 7 === o.counter ); + } ); + } ); + + describe( 'Removing actions', function() { + it( 'Should remove an action by id', function() { + const o = { counter: 0 }; + const id = add( EVERY_SECOND, nudgeObject( o, 42 ) ); + + this.clock.tick( 1000 ); + assert( 42 === o.counter ); + + remove( id ); + this.clock.tick( 1000 ); + assert( 42 === o.counter ); + } ); + + it( 'Should not decrement the next id after removing an action', function() { + add( EVERY_SECOND, noop ); + add( EVERY_FIVE_SECONDS, noop ); + + const id = add( EVERY_TEN_SECONDS, noop ); + remove( id ); + + assert( 4 === add( EVERY_THIRTY_SECONDS, noop ) ); + } ); + } ); + + describe( 'Running actions', function() { + it( 'Should run all actions for a given period when called', function() { + const o = { counter: 0 }; + + add( EVERY_SECOND, nudgeObject( o, 3 ) ); + add( EVERY_SECOND, nudgeObject( o, 5 ) ); + + this.clock.tick( 1000 ); + + assert( 3 + 5 === o.counter ); + } ); + + it( 'Should only execute actions for the given period', function() { + const o = { counter: 0 }; + + add( EVERY_SECOND, nudgeObject( o, 3 ) ); + add( EVERY_MINUTE, nudgeObject( o, 5 ) ); + + // plus 1 second + this.clock.tick( 1000 ); + assert( 3 === o.counter ); + + // plus 1 minute + this.clock.tick( 1000 * 59 ); + assert( 3 * 60 + 5 === o.counter ); + } ); + + it( 'Should only execute actions that remain after removal', function() { + const o = { counter: 0 }; + + const id = add( EVERY_SECOND, nudgeObject( o, 3 ) ); + + this.clock.tick( 1000 ); + assert( 3 === o.counter ); + + this.clock.tick( 1000 ); + assert( 6 === o.counter ); + + remove( id ); + this.clock.tick( 1000 ); + assert( 6 === o.counter ); + } ); + } ); +} ); diff --git a/package.json b/package.json index ef70ecdb647c9..8b943dba86c65 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "blanket": "1.1.6", "chai": "2.0.0", "chai-immutable": "^1.4.0", + "enzyme": "1.1.0", "esformatter": "0.7.3", "esformatter-braces": "1.2.1", "esformatter-collapse-objects-a8c": "0.1.0", From 831deceaf7f827618ce56df0ca96a0fa2d9075da Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:16:36 -0500 Subject: [PATCH 42/89] Styles: Fix indentation warnings for Reader --- client/reader/following-edit/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/reader/following-edit/style.scss b/client/reader/following-edit/style.scss index ff7df1fcdbd3f..4f2c372e88184 100644 --- a/client/reader/following-edit/style.scss +++ b/client/reader/following-edit/style.scss @@ -128,7 +128,7 @@ &:focus { border-color: #0087be; - box-shadow: 0 0 0 2px #78dcfa; + box-shadow: 0 0 0 2px #78dcfa; } } @@ -290,4 +290,4 @@ margin-right: 100px; } } -} \ No newline at end of file +} From 4e115b75b895934e3b400e7c4fa9b0d59d0521a8 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:17:55 -0500 Subject: [PATCH 43/89] Styles: Fix indentation warnings for post-editor --- client/post-editor/editor-page-parent/style.scss | 12 ++++++------ client/post-editor/media-modal/fieldset.scss | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/post-editor/editor-page-parent/style.scss b/client/post-editor/editor-page-parent/style.scss index d91bf87c70f48..502bfecb8d970 100644 --- a/client/post-editor/editor-page-parent/style.scss +++ b/client/post-editor/editor-page-parent/style.scss @@ -25,18 +25,18 @@ } .editor-page-parent .post-selector__list-item { - padding: 2px 0; - font-size: 13px; + padding: 2px 0; + font-size: 13px; } .editor-page-parent input[type=radio] { - margin-right: 8px + margin-right: 8px } .editor-page-parent .post-selector__label { - display: block; - margin-left: 24px; - margin-top: 2px; + display: block; + margin-left: 24px; + margin-top: 2px; } .editor-page-parent__label-text { diff --git a/client/post-editor/media-modal/fieldset.scss b/client/post-editor/media-modal/fieldset.scss index b521dded8ddeb..801e2afe748da 100644 --- a/client/post-editor/media-modal/fieldset.scss +++ b/client/post-editor/media-modal/fieldset.scss @@ -9,7 +9,7 @@ } .editor-media-modal__fieldset textarea { - min-height: 76px; + min-height: 76px; } .editor-media-modal__fieldset-legend { From 96bef1387bbdac15d0960fdbe6cdb66a03e4e467 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:20:02 -0500 Subject: [PATCH 44/89] Styles: Fix indentation warnings for client/my-sites --- client/my-sites/plugins/plugins-browser/style.scss | 4 ++-- client/my-sites/post-trends/style.scss | 8 ++++---- .../site-settings/settings-card-footer/style.scss | 2 +- client/my-sites/stats/stats-list/style.scss | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/my-sites/plugins/plugins-browser/style.scss b/client/my-sites/plugins/plugins-browser/style.scss index 47acad33e15ff..f3ac17509ec42 100644 --- a/client/my-sites/plugins/plugins-browser/style.scss +++ b/client/my-sites/plugins/plugins-browser/style.scss @@ -6,10 +6,10 @@ @include breakpoint( "<660px" ) { margin-top: 2em; } - } +} .plugins-browser .section-nav { @include breakpoint( "<660px" ) { margin-top: 2em; } -} \ No newline at end of file +} diff --git a/client/my-sites/post-trends/style.scss b/client/my-sites/post-trends/style.scss index f90fa7226640b..eb9370eb0dc59 100644 --- a/client/my-sites/post-trends/style.scss +++ b/client/my-sites/post-trends/style.scss @@ -161,10 +161,10 @@ } @include breakpoint( ">960px" ) { - .post-trends__scroll-left, - .post-trends__scroll-right { - display: none; - } + .post-trends__scroll-left, + .post-trends__scroll-right { + display: none; + } } @include breakpoint( "<960px" ) { diff --git a/client/my-sites/site-settings/settings-card-footer/style.scss b/client/my-sites/site-settings/settings-card-footer/style.scss index 9bb26c344b573..cfb9463624876 100644 --- a/client/my-sites/site-settings/settings-card-footer/style.scss +++ b/client/my-sites/site-settings/settings-card-footer/style.scss @@ -22,5 +22,5 @@ } .settings-card-footer .progress-indicator { - float: left; + float: left; } diff --git a/client/my-sites/stats/stats-list/style.scss b/client/my-sites/stats/stats-list/style.scss index bfa948ee131af..6141cf8ac9084 100644 --- a/client/my-sites/stats/stats-list/style.scss +++ b/client/my-sites/stats/stats-list/style.scss @@ -761,9 +761,9 @@ ul.module-content-list-legend { > .module-content-list-item-wrapper .module-content-list-item-right, > .module-content-list-item-wrapper { background: $gray-light; // Default non-active color - } + } - .module-content-list-item-right::before { + .module-content-list-item-right::before { background-image: linear-gradient(to right, $transparent 0%, $gray-light 90%); } From a669e3e03781ea29c2c883db7560e49e438604f5 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:26:26 -0500 Subject: [PATCH 45/89] Styles: Fix indentation warnings for tinymce component --- .../tinymce/plugins/calypso-alert/style.scss | 2 +- client/components/tinymce/style.scss | 42 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/client/components/tinymce/plugins/calypso-alert/style.scss b/client/components/tinymce/plugins/calypso-alert/style.scss index 3f01487249332..40da1c5d75d1d 100644 --- a/client/components/tinymce/plugins/calypso-alert/style.scss +++ b/client/components/tinymce/plugins/calypso-alert/style.scss @@ -1,5 +1,5 @@ .editor-alert-modal.dialog.card { - @include breakpoint( ">660px" ) { + @include breakpoint( ">660px" ) { max-width: 500px; } } diff --git a/client/components/tinymce/style.scss b/client/components/tinymce/style.scss index 58991e551d486..0b75280a538da 100644 --- a/client/components/tinymce/style.scss +++ b/client/components/tinymce/style.scss @@ -164,7 +164,7 @@ margin-top: 0px; -webkit-font-smoothing: auto; - -moz-osx-font-smoothing: auto; + -moz-osx-font-smoothing: auto; @include breakpoint( ">660px" ) { border-right: 1px solid lighten( $gray, 30% ); @@ -201,7 +201,7 @@ @import 'plugins/advanced/style'; /*------------------------------------------------------------------------------ - TinyMCE and Quicklinks toolbars + TinyMCE and Quicklinks toolbars ------------------------------------------------------------------------------*/ /* TinyMCE widgets/containers */ @@ -692,8 +692,8 @@ div.mce-path { } .post-editor .mce-toolbar .mce-btn-group .mce-btn.mce-listbox:hover { - background-image: none; - border-right-color: lighten( $gray, 30% ); + background-image: none; + border-right-color: lighten( $gray, 30% ); @include breakpoint( "<660px" ) { border-right-color: $white; @@ -934,7 +934,7 @@ div.mce-menu .mce-menu-item-sep, } .mce-btn-small .mce-ico { - font-family: "tinymce-small", Arial, sans-serif; + font-family: "tinymce-small", Arial, sans-serif; } .mce-toolbar .mce-ico { @@ -1631,19 +1631,19 @@ i.mce-i-wp_code:before { } #wp-fullscreen-buttons .mce-btn button { - margin: 0; - outline: 0 none; - border: 0 none; - white-space: nowrap; - width: auto; - background: none; + margin: 0; + outline: 0 none; + border: 0 none; + white-space: nowrap; + width: auto; + background: none; color: #32373c; - cursor: pointer; - font-size: 18px; - line-height: 20px; - overflow: visible; - text-align: center; - box-sizing: border-box; + cursor: pointer; + font-size: 18px; + line-height: 20px; + overflow: visible; + text-align: center; + box-sizing: border-box; } .wp-html-mode #wp-fullscreen-buttons div { @@ -1836,7 +1836,7 @@ i.mce-i-wp_code:before { } /*------------------------------------------------------------------------------ - wp-link + wp-link ------------------------------------------------------------------------------*/ div.wp-link-preview { @@ -1888,13 +1888,13 @@ div.wp-link-preview a { html:lang(he-il) .rtl .wp-switch-editor, html:lang(he-il) .rtl .quicktags-toolbar input { - font-family: Arial, sans-serif; + font-family: Arial, sans-serif; } /* HiDPI */ @media print, - (-webkit-min-device-pixel-ratio: 1.25), - (min-resolution: 120dpi) { +(-webkit-min-device-pixel-ratio: 1.25), +(min-resolution: 120dpi) { .wp-media-buttons .add_media span.wp-media-buttons-icon, #wp-fullscreen-buttons #wp_fs_image span.mce_image { background: none; From 5480c92ccea1febe89b01db194a4429a5eb60b89 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:28:04 -0500 Subject: [PATCH 46/89] Styles: Fix indentation warnings for client/foldable-card --- client/components/foldable-card/style.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/components/foldable-card/style.scss b/client/components/foldable-card/style.scss index d9e6e47b401c3..eee3dcda851a3 100644 --- a/client/components/foldable-card/style.scss +++ b/client/components/foldable-card/style.scss @@ -80,7 +80,7 @@ button.foldable-card__action { display: flex; align-items: center; flex: 1 1; - justify-content: flex-end; + justify-content: flex-end; } .foldable-card__expand { @@ -135,12 +135,12 @@ button.foldable-card__action { .foldable-card.has-expanded-summary & { transition: none; flex: 2; - text-align: right; + text-align: right; } @include breakpoint( "<480px" ) { display: none; - } + } } From c5ea2fa145a602e43516333d26c33e828f0fd661 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:32:00 -0500 Subject: [PATCH 47/89] Framework: Fix indentation for client/reader/share --- client/reader/share/index.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/reader/share/index.jsx b/client/reader/share/index.jsx index 025dd1e198857..f5574acab19b8 100644 --- a/client/reader/share/index.jsx +++ b/client/reader/share/index.jsx @@ -73,11 +73,11 @@ const twitterIcon = ( - - - + + + ), facebookIcon = ( Date: Thu, 10 Dec 2015 16:32:36 -0500 Subject: [PATCH 48/89] Framework: Fix indentation for server/api --- server/api/oauth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/api/oauth.js b/server/api/oauth.js index 540ac6d5aa697..40b81f0ca2969 100644 --- a/server/api/oauth.js +++ b/server/api/oauth.js @@ -4,9 +4,9 @@ var req = require( 'superagent' ), bodyParser = require( 'body-parser' ); - /** - * Internal dependencies - */ +/** + * Internal dependencies + */ var config = require( 'config' ); function oauth() { From 9e691e71f0a2d61680204f7194755744464538df Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:33:17 -0500 Subject: [PATCH 49/89] Framework: Fix indentation for post-editor/editor-tags --- client/post-editor/editor-tags/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/post-editor/editor-tags/index.jsx b/client/post-editor/editor-tags/index.jsx index 65bc67b380955..3f354606d2231 100644 --- a/client/post-editor/editor-tags/index.jsx +++ b/client/post-editor/editor-tags/index.jsx @@ -15,7 +15,7 @@ import { recordStat, recordEvent } from 'lib/posts/stats'; import { isPage } from 'lib/posts/utils'; import InfoPopover from 'components/info-popover'; - const debug = _debug( 'calypso:post-editor:editor-tags' ); +const debug = _debug( 'calypso:post-editor:editor-tags' ); module.exports = React.createClass( { displayName: 'EditorTags', From 9beef2001524a3855963022bc598b85b9f8567c2 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:34:57 -0500 Subject: [PATCH 50/89] Framework: Fix indentation for my-sites/stats --- client/my-sites/stats/geochart/index.jsx | 4 ++-- client/my-sites/stats/pagination/pagination-page.jsx | 2 +- client/my-sites/stats/stats-list/index.jsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/my-sites/stats/geochart/index.jsx b/client/my-sites/stats/geochart/index.jsx index 1c0a1eaf68411..47730dc24f4e5 100644 --- a/client/my-sites/stats/geochart/index.jsx +++ b/client/my-sites/stats/geochart/index.jsx @@ -8,7 +8,7 @@ var React = require( 'react/addons' ), /** * Internal dependencies */ - var analytics = require( 'analytics' ); +var analytics = require( 'analytics' ); module.exports = React.createClass( { displayName: 'StatsGeochart', @@ -130,4 +130,4 @@ module.exports = React.createClass( { return (
    ); } -} ); \ No newline at end of file +} ); diff --git a/client/my-sites/stats/pagination/pagination-page.jsx b/client/my-sites/stats/pagination/pagination-page.jsx index 019e6a5c1fabc..7e52aa97953c9 100644 --- a/client/my-sites/stats/pagination/pagination-page.jsx +++ b/client/my-sites/stats/pagination/pagination-page.jsx @@ -7,7 +7,7 @@ var React = require( 'react' ), /** * Internal dependencies */ - var Gridicon = require( 'components/gridicon' ); +var Gridicon = require( 'components/gridicon' ); module.exports = React.createClass( { diff --git a/client/my-sites/stats/stats-list/index.jsx b/client/my-sites/stats/stats-list/index.jsx index 0246a22dee715..6ad44c9f41e19 100644 --- a/client/my-sites/stats/stats-list/index.jsx +++ b/client/my-sites/stats/stats-list/index.jsx @@ -8,7 +8,7 @@ var React = require( 'react' ), /** * Internal dependencies */ - var StatsListItem = require( './stats-list-item' ); +var StatsListItem = require( './stats-list-item' ); module.exports = React.createClass( { displayName: 'StatsList', From 80ad9862ef48deaf9f8f02ddd94d0bae3321f0cf Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:36:14 -0500 Subject: [PATCH 51/89] Framework: Fix indentation for my-sites/media --- client/my-sites/media/main.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/my-sites/media/main.jsx b/client/my-sites/media/main.jsx index c00363eeb2643..d5acf673962b6 100644 --- a/client/my-sites/media/main.jsx +++ b/client/my-sites/media/main.jsx @@ -8,8 +8,8 @@ var React = require( 'react' ), * Internal dependencies */ var MediaLibrary = require( 'my-sites/media-library' ), - SidebarNavigation = require( 'my-sites/sidebar-navigation' ), - observe = require( 'lib/mixins/data-observe' ); + SidebarNavigation = require( 'my-sites/sidebar-navigation' ), + observe = require( 'lib/mixins/data-observe' ); module.exports = React.createClass( { displayName: 'Media', From 6b174a956069a1b64cfcdad9a8c8a0b6cda6cb41 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:36:58 -0500 Subject: [PATCH 52/89] Framework: Fix indentation for me/profile-links --- client/me/profile-links/add-buttons.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/me/profile-links/add-buttons.jsx b/client/me/profile-links/add-buttons.jsx index fec8dbc2e76e8..56bd82a9747bf 100644 --- a/client/me/profile-links/add-buttons.jsx +++ b/client/me/profile-links/add-buttons.jsx @@ -1,7 +1,7 @@ /** * External dependencies */ - var React = require( 'react' ); +var React = require( 'react' ); // Internal dependencies var Button = require( 'components/button' ), From 7db23f53c6908af9cffbd952b6f6daa29a17ea00 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:39:32 -0500 Subject: [PATCH 53/89] Framework: Fix indentation for client/lib --- client/lib/credit-card-details/test/test.js | 2 +- client/lib/feed-stream-store/feed-stream.js | 4 ++-- client/lib/invites/reducers/invites-list.js | 2 +- client/lib/invites/reducers/invites-validation.js | 2 +- client/lib/posts/post-counts-store.js | 2 +- client/lib/touch-detect/index.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/lib/credit-card-details/test/test.js b/client/lib/credit-card-details/test/test.js index 2bb2cd915bd1b..3fee028a00d20 100644 --- a/client/lib/credit-card-details/test/test.js +++ b/client/lib/credit-card-details/test/test.js @@ -9,7 +9,7 @@ var assert = require( 'assert' ); var creditCardDetails = require( '../' ); function getRandomInt( min, max ) { - return Math.floor(Math.random() * (max - min + 1)) + min; + return Math.floor(Math.random() * (max - min + 1)) + min; } describe( 'credit-card-details', function() { diff --git a/client/lib/feed-stream-store/feed-stream.js b/client/lib/feed-stream-store/feed-stream.js index 252281509adfc..c930d3a0e318a 100644 --- a/client/lib/feed-stream-store/feed-stream.js +++ b/client/lib/feed-stream-store/feed-stream.js @@ -380,8 +380,8 @@ assign( FeedStream.prototype, { }, /** - * Process a new page of data and concatenate to the end of the list - **/ + * Process a new page of data and concatenate to the end of the list + **/ receivePage: function( id, error, data ) { var posts, postKeys; diff --git a/client/lib/invites/reducers/invites-list.js b/client/lib/invites/reducers/invites-list.js index e65ace1eb4bac..3a40d2a857558 100644 --- a/client/lib/invites/reducers/invites-list.js +++ b/client/lib/invites/reducers/invites-list.js @@ -33,6 +33,6 @@ const reducer = ( state = initialState, payload ) => { return state.setIn( [ 'errors', action.siteId ], action.error ); } return state; - } +} export { initialState, reducer }; diff --git a/client/lib/invites/reducers/invites-validation.js b/client/lib/invites/reducers/invites-validation.js index fed113d9168f6..0db1d20ff36e8 100644 --- a/client/lib/invites/reducers/invites-validation.js +++ b/client/lib/invites/reducers/invites-validation.js @@ -45,6 +45,6 @@ const reducer = ( state = initialState, payload ) => { return state.setIn( [ 'errors', action.siteId, action.inviteKey ], action.error ); } return state; - } +} export { initialState, reducer }; diff --git a/client/lib/posts/post-counts-store.js b/client/lib/posts/post-counts-store.js index 67ffc56072d97..0294b9c4807cb 100644 --- a/client/lib/posts/post-counts-store.js +++ b/client/lib/posts/post-counts-store.js @@ -181,7 +181,7 @@ function updateCountsWhenPostChanges( post, original ) { PostCountsStore.emit( 'change' ); } - /* +/* * Update post counts when a post is created * * @param {Object} post - current post state diff --git a/client/lib/touch-detect/index.js b/client/lib/touch-detect/index.js index 9162e6c1dbb87..58fd4b821d54b 100644 --- a/client/lib/touch-detect/index.js +++ b/client/lib/touch-detect/index.js @@ -12,7 +12,7 @@ module.exports = { * @see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js * * @returns {Boolean} whether touch screen is available - */ + */ hasTouch: function() { /* global DocumentTouch:true */ return ( ( 'ontouchstart' in window ) || window.DocumentTouch && document instanceof DocumentTouch ); From 349d6481089db7603982383e626d8021606b34e6 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:41:30 -0500 Subject: [PATCH 54/89] Framework: Fix indentation for components/tinymce/plugins --- client/components/tinymce/plugins/wpcom/plugin.js | 4 ++-- client/components/tinymce/plugins/wpeditimage/plugin.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/components/tinymce/plugins/wpcom/plugin.js b/client/components/tinymce/plugins/wpcom/plugin.js index 0ab353a180a20..c7b89d74bc90b 100644 --- a/client/components/tinymce/plugins/wpcom/plugin.js +++ b/client/components/tinymce/plugins/wpcom/plugin.js @@ -23,8 +23,8 @@ function wpcomPlugin( editor ) { style; editor.on( 'focus', function() { - window.wpActiveEditor = editor.id; - } ); + window.wpActiveEditor = editor.id; + } ); // Replace Read More/Next Page tags with images and apply wpautop editor.on( 'BeforeSetContent', function( event ) { diff --git a/client/components/tinymce/plugins/wpeditimage/plugin.js b/client/components/tinymce/plugins/wpeditimage/plugin.js index e5568b6946f31..20c042e543913 100644 --- a/client/components/tinymce/plugins/wpeditimage/plugin.js +++ b/client/components/tinymce/plugins/wpeditimage/plugin.js @@ -545,7 +545,7 @@ function wpEditImage( editor ) { } } ); } - } ); + } ); editor.on( 'BeforeExecCommand', function( event ) { var node, p, DL, align, replacement, From 19a85b25371cff7d50b24d73e2e6de900b44cd15 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:42:17 -0500 Subject: [PATCH 55/89] Framework: Fix indentation for plans component --- client/components/plans/plan-discount-message/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/plans/plan-discount-message/index.jsx b/client/components/plans/plan-discount-message/index.jsx index 2d9c67358ff84..498f028ca8072 100644 --- a/client/components/plans/plan-discount-message/index.jsx +++ b/client/components/plans/plan-discount-message/index.jsx @@ -6,7 +6,7 @@ var React = require( 'react' ); /** * Internal dependencies */ - var productsValues = require( 'lib/products-values' ); +var productsValues = require( 'lib/products-values' ); module.exports = React.createClass( { displayName: 'PlanDiscountMessage', From b2c99741c890a869fe364313d6b78559459604d6 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:43:03 -0500 Subject: [PATCH 56/89] Framework: Fix indentation in overlay component --- client/components/overlay/overlay.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/overlay/overlay.jsx b/client/components/overlay/overlay.jsx index d428302d6d2c6..69d3b5f3ee576 100644 --- a/client/components/overlay/overlay.jsx +++ b/client/components/overlay/overlay.jsx @@ -47,10 +47,10 @@ module.exports = React.createClass({ }, /** - * When overlay is going to be unmounted remove the `overlay-open` + * When overlay is going to be unmounted remove the `overlay-open` * class from the document html element to animate it out * and remove `overlay-is-front` when the animation has completed - */ + */ componentWillUnmount: function() { debug( 'Unmounting overlay component.' ); classes( document.documentElement ).remove( 'overlay-open' ).remove( 'animate' ); From 70fa052384d1f29bad564879cee389a1f244967e Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 16:43:34 -0500 Subject: [PATCH 57/89] Framework: Fix indentation in auth/login --- client/auth/login.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/auth/login.jsx b/client/auth/login.jsx index 9a87119f1b528..09774820c4ba1 100644 --- a/client/auth/login.jsx +++ b/client/auth/login.jsx @@ -28,7 +28,7 @@ const LostPassword = React.createClass( { { this.translate( 'Lost your password?' ) }

    - ); + ); } } ); From dc75c3247cb66a6d96effaf9754ab122a4382d2c Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Thu, 10 Dec 2015 14:51:13 -0800 Subject: [PATCH 58/89] Section Header: move to a standard font size of 11px --- client/components/section-header/style.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/components/section-header/style.scss b/client/components/section-header/style.scss index c7616f8db8602..d149a5b5c3edf 100644 --- a/client/components/section-header/style.scss +++ b/client/components/section-header/style.scss @@ -36,13 +36,12 @@ .section-header__label, .section-header__button { color: $gray; - font-size: 12px; + font-size: 11px; text-transform: uppercase; } .section-header__button { background: none; - float: let; margin-right: 8px; padding: 2px 8px; From b6668f9aca7f533788baa301856a642b7e88d123 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 25 Nov 2015 09:52:05 -0500 Subject: [PATCH 59/89] Sites: Introduce Redux-based sites module Work-in-progress, including only selected site logic --- shared/lib/sites/Makefile | 10 ++++ shared/lib/sites/README.md | 56 ++++++++++++++++++ shared/lib/sites/action-types.js | 2 + shared/lib/sites/actions.js | 35 ++++++++++++ shared/lib/sites/reducers.js | 53 +++++++++++++++++ shared/lib/sites/selectors.js | 15 +++++ shared/lib/sites/test/actions.js | 41 ++++++++++++++ shared/lib/sites/test/reducers.js | 91 ++++++++++++++++++++++++++++++ shared/lib/sites/test/selectors.js | 36 ++++++++++++ 9 files changed, 339 insertions(+) create mode 100644 shared/lib/sites/Makefile create mode 100644 shared/lib/sites/README.md create mode 100644 shared/lib/sites/action-types.js create mode 100644 shared/lib/sites/actions.js create mode 100644 shared/lib/sites/reducers.js create mode 100644 shared/lib/sites/selectors.js create mode 100644 shared/lib/sites/test/actions.js create mode 100644 shared/lib/sites/test/reducers.js create mode 100644 shared/lib/sites/test/selectors.js diff --git a/shared/lib/sites/Makefile b/shared/lib/sites/Makefile new file mode 100644 index 0000000000000..67b5631ee05af --- /dev/null +++ b/shared/lib/sites/Makefile @@ -0,0 +1,10 @@ +REPORTER ?= spec +NODE_BIN := $(shell npm bin) +MOCHA ?= $(NODE_BIN)/mocha +BASE_DIR := $(NODE_BIN)/../.. +NODE_PATH := $(BASE_DIR)/client:$(BASE_DIR)/shared + +test: + @NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers jsx:babel/register,js:babel/register --reporter $(REPORTER) + +.PHONY: test diff --git a/shared/lib/sites/README.md b/shared/lib/sites/README.md new file mode 100644 index 0000000000000..8c6819c5a9e66 --- /dev/null +++ b/shared/lib/sites/README.md @@ -0,0 +1,56 @@ +Sites +===== + +A module for managing site data, including the sites themselves and related behaviors, most common of which is the currently selected site. + +__Note:__ This module does not yet have complete feature parity with [`sites-list`](../../../client/lib/sites-list). Refer to the set of actions and reducers below to determine whether your needs are satisfied. If not, consider adding support to the module, or use `sites-list` instead. + +## Actions + +Used in combination with the Redux store instance `dispatch` function, actions can be used in manipulating the current global state. + +### `setSelectedSite( siteId: Number )` + +Sets the currently selected site, by site ID. + +```js +import { setSelectedSite } from 'lib/sites/actions'; + +dispatch( setSelectedSite( 2916284 ) ); +``` + +### `receiveSite( site: Object )` + +Adds a site object to the set of known sites. + +```js +import { receiveSite } from 'lib/sites/actions'; + +dispatch( receiveSite( { ID: 2916284, name: 'WordPress.com Example Blog' } ) ); +``` + +## Reducers + +The Sites reducers add the following keys to the global state tree, under `sites`: + +#### `selected` + +The currently selected site, or `null` if no site is selected. + +#### `byId` + +All known sites, indexed by site ID. + +## Selectors + +Selectors are intended to assist in extracting data from the global state tree for consumption by other modules. + +#### `getSelectedSite( state: Object )` + +Returns the currently selected site object. + +```js +import { getSelectedSite } from 'lib/sites/selectors'; + +const selectedSite = getSelectedSite( store.getState() ); +``` diff --git a/shared/lib/sites/action-types.js b/shared/lib/sites/action-types.js new file mode 100644 index 0000000000000..4a7b9ed52affd --- /dev/null +++ b/shared/lib/sites/action-types.js @@ -0,0 +1,2 @@ +export const SET_SELECTED_SITE = 'SET_SELECTED_SITE'; +export const RECEIVE_SITE = 'RECEIVE_SITE'; diff --git a/shared/lib/sites/actions.js b/shared/lib/sites/actions.js new file mode 100644 index 0000000000000..bea2f1c5dcf4a --- /dev/null +++ b/shared/lib/sites/actions.js @@ -0,0 +1,35 @@ +/** + * Internal dependencies + */ +import { + SET_SELECTED_SITE, + RECEIVE_SITE +} from './action-types'; + +/** + * Returns an action object to be used in signalling that a site has been set + * as selected. + * + * @param {Number} siteId Site ID + * @return {Object} Action object + */ +export function setSelectedSite( siteId ) { + return { + type: SET_SELECTED_SITE, + siteId + }; +} + +/** + * Returns an action object to be used in signalling that a site object has + * been received. + * + * @param {Object} site Site received + * @return {Object} Action object + */ +export function receiveSite( site ) { + return { + type: RECEIVE_SITE, + site + }; +} diff --git a/shared/lib/sites/reducers.js b/shared/lib/sites/reducers.js new file mode 100644 index 0000000000000..da1a13d0c9c09 --- /dev/null +++ b/shared/lib/sites/reducers.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { combineReducers } from 'redux'; + +/** + * Internal dependencies + */ +import { + SET_SELECTED_SITE, + RECEIVE_SITE +} from './action-types'; + +/** + * Tracks the currently selected site ID. + * + * @param {Object} state Current state + * @param {Object} action Action payload + * @return {Object} Updated state + */ +export function selected( state = null, action ) { + switch ( action.type ) { + case SET_SELECTED_SITE: + state = action.siteId; + break; + } + + return state; +} + +/** + * Tracks all known site objects, indexed by site ID. + * + * @param {Object} state Current state + * @param {Object} action Action payload + * @return {Object} Updated state + */ +export function byId( state = {}, action ) { + switch ( action.type ) { + case RECEIVE_SITE: + state = Object.assign( {}, state, { + [ action.site.ID ]: action.site + } ); + break; + } + + return state; +} + +export default combineReducers( { + selected, + byId +} ); diff --git a/shared/lib/sites/selectors.js b/shared/lib/sites/selectors.js new file mode 100644 index 0000000000000..183438b7a8efe --- /dev/null +++ b/shared/lib/sites/selectors.js @@ -0,0 +1,15 @@ +/** + * Returns the site object for the currently selected site. + * + * @param {Object} state Global state tree + * @return {Object} Selected site + */ +export function getSelectedSite( state ) { + const { selected, byId } = state.sites; + + if ( ! selected ) { + return null; + } + + return byId[ selected ]; +} diff --git a/shared/lib/sites/test/actions.js b/shared/lib/sites/test/actions.js new file mode 100644 index 0000000000000..e52f83e8db89c --- /dev/null +++ b/shared/lib/sites/test/actions.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { + SET_SELECTED_SITE, + RECEIVE_SITE +} from '../action-types'; +import { + setSelectedSite, + receiveSite +} from '../actions'; + +describe( 'actions', () => { + describe( '#setSelectedSite()', () => { + it( 'should return an action object', () => { + const action = setSelectedSite( 2916284 ); + + expect( action ).to.eql( { + type: SET_SELECTED_SITE, + siteId: 2916284 + } ); + } ); + } ); + + describe( '#receiveSite()', () => { + it( 'should return an action object', () => { + const site = { ID: 2916284, name: 'WordPress.com Example Blog' }; + const action = receiveSite( site ); + + expect( action ).to.eql( { + type: RECEIVE_SITE, + site + } ); + } ); + } ); +} ); diff --git a/shared/lib/sites/test/reducers.js b/shared/lib/sites/test/reducers.js new file mode 100644 index 0000000000000..7bd22fe37e471 --- /dev/null +++ b/shared/lib/sites/test/reducers.js @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { + SET_SELECTED_SITE, + RECEIVE_SITE +} from '../action-types'; +import { + selected, + byId +} from '../reducers'; + +describe( 'reducers', () => { + describe( '#selected()', () => { + it( 'should default to null', () => { + const state = selected( undefined, {} ); + + expect( state ).to.be.null; + } ); + + it( 'should set the selected site ID', () => { + const state = selected( null, { + type: SET_SELECTED_SITE, + siteId: 2916284 + } ); + + expect( state ).to.equal( 2916284 ); + } ); + } ); + + describe( '#byId()', () => { + it( 'should default to an empty object', () => { + const state = byId( undefined, {} ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should index sites by ID', () => { + const state = byId( null, { + type: RECEIVE_SITE, + site: { ID: 2916284, name: 'WordPress.com Example Blog' } + } ); + + expect( state ).to.eql( { + 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } + } ); + } ); + + it( 'should return a new instance of the state object', () => { + const original = {}; + const state = byId( original, { + type: RECEIVE_SITE, + site: { ID: 2916284, name: 'WordPress.com Example Blog' } + } ); + + expect( state ).to.not.equal( original ); + } ); + + it( 'should accumulate sites', () => { + const state = byId( { + 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } + }, { + type: RECEIVE_SITE, + site: { ID: 77203074, name: 'Just You Wait' } + } ); + + expect( state ).to.eql( { + 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' }, + 77203074: { ID: 77203074, name: 'Just You Wait' } + } ); + } ); + + it( 'should override previous site of same ID', () => { + const state = byId( { + 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } + }, { + type: RECEIVE_SITE, + site: { ID: 2916284, name: 'Just You Wait' } + } ); + + expect( state ).to.eql( { + 2916284: { ID: 2916284, name: 'Just You Wait' } + } ); + } ); + } ); +} ); diff --git a/shared/lib/sites/test/selectors.js b/shared/lib/sites/test/selectors.js new file mode 100644 index 0000000000000..18e7183762217 --- /dev/null +++ b/shared/lib/sites/test/selectors.js @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { getSelectedSite } from '../selectors'; + +describe( 'selectors', () => { + describe( '#getSelectedSite()', () => { + it( 'should return null if no site is selected', () => { + const selected = getSelectedSite( { + sites: { + selected: null + } + } ); + + expect( selected ).to.be.null; + } ); + + it( 'should return the object for the selected site', () => { + const selected = getSelectedSite( { + sites: { + selected: 2916284, + byId: { + 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } + } + } + } ); + + expect( selected ).to.eql( { ID: 2916284, name: 'WordPress.com Example Blog' } ); + } ); + } ); +} ); From 39486cc5e4ee4221e88f424d37ca05faed96fc8a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 7 Dec 2015 15:11:52 -0500 Subject: [PATCH 60/89] Framework: Move Redux state logic to state directory --- CREDITS.md | 4 +-- client/boot/index.js | 2 +- .../post-editor/editor-sharing/container.jsx | 4 +-- client/state/README.md | 14 +++++++++++ client/state/index.js | 25 +++++++++++++++++++ .../lib => client/state}/sharing/README.md | 8 +++--- .../state}/sharing/action-types.js | 0 .../state}/sharing/publicize/Makefile | 0 .../state}/sharing/publicize/actions.js | 0 .../state}/sharing/publicize/reducers.js | 0 .../state}/sharing/publicize/selectors.js | 0 .../state}/sharing/publicize/test/actions.js | 0 .../state}/sharing/publicize/test/reducers.js | 0 .../sharing/publicize/test/selectors.js | 0 .../lib => client/state}/sharing/reducers.js | 0 {shared/lib => client/state}/sites/Makefile | 0 {shared/lib => client/state}/sites/README.md | 6 ++--- .../state}/sites/action-types.js | 0 {shared/lib => client/state}/sites/actions.js | 0 .../lib => client/state}/sites/reducers.js | 0 .../lib => client/state}/sites/selectors.js | 0 .../state}/sites/test/actions.js | 0 .../state}/sites/test/reducers.js | 0 .../state}/sites/test/selectors.js | 0 shared/lib/create-redux-store/README.md | 12 --------- shared/lib/create-redux-store/index.js | 16 ------------ shared/lib/create-redux-store/reducers.js | 13 ---------- 27 files changed, 51 insertions(+), 53 deletions(-) create mode 100644 client/state/README.md create mode 100644 client/state/index.js rename {shared/lib => client/state}/sharing/README.md (86%) rename {shared/lib => client/state}/sharing/action-types.js (100%) rename {shared/lib => client/state}/sharing/publicize/Makefile (100%) rename {shared/lib => client/state}/sharing/publicize/actions.js (100%) rename {shared/lib => client/state}/sharing/publicize/reducers.js (100%) rename {shared/lib => client/state}/sharing/publicize/selectors.js (100%) rename {shared/lib => client/state}/sharing/publicize/test/actions.js (100%) rename {shared/lib => client/state}/sharing/publicize/test/reducers.js (100%) rename {shared/lib => client/state}/sharing/publicize/test/selectors.js (100%) rename {shared/lib => client/state}/sharing/reducers.js (100%) rename {shared/lib => client/state}/sites/Makefile (100%) rename {shared/lib => client/state}/sites/README.md (89%) rename {shared/lib => client/state}/sites/action-types.js (100%) rename {shared/lib => client/state}/sites/actions.js (100%) rename {shared/lib => client/state}/sites/reducers.js (100%) rename {shared/lib => client/state}/sites/selectors.js (100%) rename {shared/lib => client/state}/sites/test/actions.js (100%) rename {shared/lib => client/state}/sites/test/reducers.js (100%) rename {shared/lib => client/state}/sites/test/selectors.js (100%) delete mode 100644 shared/lib/create-redux-store/README.md delete mode 100644 shared/lib/create-redux-store/index.js delete mode 100644 shared/lib/create-redux-store/reducers.js diff --git a/CREDITS.md b/CREDITS.md index dad1aa1859f9f..bbea6065411c6 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1174,7 +1174,7 @@ Permission kindly granted by Ben Congleton, of Olark. ``` ### https://github.com/rackt/redux/ -#### shared/lib/create-redux-store +#### client/state ```text The MIT License (MIT) @@ -1188,7 +1188,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ``` ### https://github.com/gaearon/redux-thunk -#### shared/lib/create-redux-store +#### client/state ```text The MIT License (MIT) diff --git a/client/boot/index.js b/client/boot/index.js index 6e61144489370..8239b2ec9a174 100644 --- a/client/boot/index.js +++ b/client/boot/index.js @@ -37,7 +37,7 @@ var config = require( 'config' ), touchDetect = require( 'lib/touch-detect' ), accessibleFocus = require( 'lib/accessible-focus' ), TitleStore = require( 'lib/screen-title/store' ), - createReduxStore = require( 'lib/create-redux-store' ), + createReduxStore = require( 'state' ).createReduxStore, // The following mixins require i18n content, so must be required after i18n is initialized Layout, LoggedOutLayout; diff --git a/client/post-editor/editor-sharing/container.jsx b/client/post-editor/editor-sharing/container.jsx index ee69eb7b9e9e7..9cc33e4a92a6c 100644 --- a/client/post-editor/editor-sharing/container.jsx +++ b/client/post-editor/editor-sharing/container.jsx @@ -8,8 +8,8 @@ import { connect } from 'react-redux'; * Internal dependencies */ import PostEditStore from 'lib/posts/post-edit-store'; -import { fetchConnections } from 'lib/sharing/publicize/actions'; -import { getConnectionsBySiteIdAvailableToCurrentUser, hasFetchedConnections } from 'lib/sharing/publicize/selectors'; +import { fetchConnections } from 'state/sharing/publicize/actions'; +import { getConnectionsBySiteIdAvailableToCurrentUser, hasFetchedConnections } from 'state/sharing/publicize/selectors'; import EditorSharingAccordion from './accordion'; class EditorSharingContainer extends Component { diff --git a/client/state/README.md b/client/state/README.md new file mode 100644 index 0000000000000..d81a1193fa9f2 --- /dev/null +++ b/client/state/README.md @@ -0,0 +1,14 @@ +State +===== + +This directory contains all of the behavior describing the global application state. Folders within this directory reflect sub-trees within the global state tree, each with their own set of actions, reducers, and utilities. + +The root module exports a single function which, when invoked, returns an instance of a [Redux store](http://redux.js.org/docs/basics/Store.html). The store instance runs dispatched actions against all known reducers. To include a reducer in the global store, simply add the reducing function to the combined reducer in `index.js`. + +## Usage + +```js +import { createReduxStore } from 'state'; + +const store = createReduxStore(); +``` diff --git a/client/state/index.js b/client/state/index.js new file mode 100644 index 0000000000000..bf6cea1aa5f25 --- /dev/null +++ b/client/state/index.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import thunkMiddleware from 'redux-thunk'; +import { createStore, applyMiddleware, combineReducers } from 'redux'; + +/** + * Internal dependencies + */ +import sharing from './sharing/reducers'; +import sites from './sites/reducers'; + +/** + * Module variables + */ +const reducer = combineReducers( { + sharing, + sites +} ); + +export function createReduxStore() { + return applyMiddleware( + thunkMiddleware + )( createStore )( reducer ); +}; diff --git a/shared/lib/sharing/README.md b/client/state/sharing/README.md similarity index 86% rename from shared/lib/sharing/README.md rename to client/state/sharing/README.md index 77be2cdb3b135..f368063dd9baa 100644 --- a/shared/lib/sharing/README.md +++ b/client/state/sharing/README.md @@ -16,7 +16,7 @@ Used in combination with the Redux store instance `dispatch` function, actions c Triggers a network request to retrieve Publicize connections for the specified site ID from the WordPress.com REST API. ```js -import { fetchConnections } from 'lib/sharing/publicize/actions'; +import { fetchConnections } from 'state/sharing/publicize/actions'; dispatch( fetchConnections( 2916284 ) ); ``` @@ -46,7 +46,7 @@ Selectors are intended to assist in extracting data from the global state tree f Returns an array of known connections for the given site ID. ```js -import { getConnectionsBySiteId } from 'lib/sharing/publicize/selectors'; +import { getConnectionsBySiteId } from 'state/sharing/publicize/selectors'; const connections = getConnectionsBySiteId( store.getState(), 2916284 ); ``` @@ -56,7 +56,7 @@ const connections = getConnectionsBySiteId( store.getState(), 2916284 ); Returns true if connections have been fetched for the given site ID. ```js -import { hasFetchedConnections } from 'lib/sharing/publicize/selectors'; +import { hasFetchedConnections } from 'state/sharing/publicize/selectors'; const connections = hasFetchedConnections( store.getState(), 2916284 ); ``` @@ -66,7 +66,7 @@ const connections = hasFetchedConnections( store.getState(), 2916284 ); Returns true if connections are currently fetching for the given site ID. ```js -import { isFetchingConnections } from 'lib/sharing/publicize/selectors'; +import { isFetchingConnections } from 'state/sharing/publicize/selectors'; const connections = isFetchingConnections( store.getState(), 2916284 ); ``` diff --git a/shared/lib/sharing/action-types.js b/client/state/sharing/action-types.js similarity index 100% rename from shared/lib/sharing/action-types.js rename to client/state/sharing/action-types.js diff --git a/shared/lib/sharing/publicize/Makefile b/client/state/sharing/publicize/Makefile similarity index 100% rename from shared/lib/sharing/publicize/Makefile rename to client/state/sharing/publicize/Makefile diff --git a/shared/lib/sharing/publicize/actions.js b/client/state/sharing/publicize/actions.js similarity index 100% rename from shared/lib/sharing/publicize/actions.js rename to client/state/sharing/publicize/actions.js diff --git a/shared/lib/sharing/publicize/reducers.js b/client/state/sharing/publicize/reducers.js similarity index 100% rename from shared/lib/sharing/publicize/reducers.js rename to client/state/sharing/publicize/reducers.js diff --git a/shared/lib/sharing/publicize/selectors.js b/client/state/sharing/publicize/selectors.js similarity index 100% rename from shared/lib/sharing/publicize/selectors.js rename to client/state/sharing/publicize/selectors.js diff --git a/shared/lib/sharing/publicize/test/actions.js b/client/state/sharing/publicize/test/actions.js similarity index 100% rename from shared/lib/sharing/publicize/test/actions.js rename to client/state/sharing/publicize/test/actions.js diff --git a/shared/lib/sharing/publicize/test/reducers.js b/client/state/sharing/publicize/test/reducers.js similarity index 100% rename from shared/lib/sharing/publicize/test/reducers.js rename to client/state/sharing/publicize/test/reducers.js diff --git a/shared/lib/sharing/publicize/test/selectors.js b/client/state/sharing/publicize/test/selectors.js similarity index 100% rename from shared/lib/sharing/publicize/test/selectors.js rename to client/state/sharing/publicize/test/selectors.js diff --git a/shared/lib/sharing/reducers.js b/client/state/sharing/reducers.js similarity index 100% rename from shared/lib/sharing/reducers.js rename to client/state/sharing/reducers.js diff --git a/shared/lib/sites/Makefile b/client/state/sites/Makefile similarity index 100% rename from shared/lib/sites/Makefile rename to client/state/sites/Makefile diff --git a/shared/lib/sites/README.md b/client/state/sites/README.md similarity index 89% rename from shared/lib/sites/README.md rename to client/state/sites/README.md index 8c6819c5a9e66..d7cb042de564e 100644 --- a/shared/lib/sites/README.md +++ b/client/state/sites/README.md @@ -14,7 +14,7 @@ Used in combination with the Redux store instance `dispatch` function, actions c Sets the currently selected site, by site ID. ```js -import { setSelectedSite } from 'lib/sites/actions'; +import { setSelectedSite } from 'state/sites/actions'; dispatch( setSelectedSite( 2916284 ) ); ``` @@ -24,7 +24,7 @@ dispatch( setSelectedSite( 2916284 ) ); Adds a site object to the set of known sites. ```js -import { receiveSite } from 'lib/sites/actions'; +import { receiveSite } from 'state/sites/actions'; dispatch( receiveSite( { ID: 2916284, name: 'WordPress.com Example Blog' } ) ); ``` @@ -50,7 +50,7 @@ Selectors are intended to assist in extracting data from the global state tree f Returns the currently selected site object. ```js -import { getSelectedSite } from 'lib/sites/selectors'; +import { getSelectedSite } from 'state/sites/selectors'; const selectedSite = getSelectedSite( store.getState() ); ``` diff --git a/shared/lib/sites/action-types.js b/client/state/sites/action-types.js similarity index 100% rename from shared/lib/sites/action-types.js rename to client/state/sites/action-types.js diff --git a/shared/lib/sites/actions.js b/client/state/sites/actions.js similarity index 100% rename from shared/lib/sites/actions.js rename to client/state/sites/actions.js diff --git a/shared/lib/sites/reducers.js b/client/state/sites/reducers.js similarity index 100% rename from shared/lib/sites/reducers.js rename to client/state/sites/reducers.js diff --git a/shared/lib/sites/selectors.js b/client/state/sites/selectors.js similarity index 100% rename from shared/lib/sites/selectors.js rename to client/state/sites/selectors.js diff --git a/shared/lib/sites/test/actions.js b/client/state/sites/test/actions.js similarity index 100% rename from shared/lib/sites/test/actions.js rename to client/state/sites/test/actions.js diff --git a/shared/lib/sites/test/reducers.js b/client/state/sites/test/reducers.js similarity index 100% rename from shared/lib/sites/test/reducers.js rename to client/state/sites/test/reducers.js diff --git a/shared/lib/sites/test/selectors.js b/client/state/sites/test/selectors.js similarity index 100% rename from shared/lib/sites/test/selectors.js rename to client/state/sites/test/selectors.js diff --git a/shared/lib/create-redux-store/README.md b/shared/lib/create-redux-store/README.md deleted file mode 100644 index e0542c193f233..0000000000000 --- a/shared/lib/create-redux-store/README.md +++ /dev/null @@ -1,12 +0,0 @@ -`create-redux-store` -==================== - -This module exports a function which, when invoked, returns an instance of a [Redux store](http://redux.js.org/docs/basics/Store.html). The store instance is intended to reflect the global state of the application, and runs dispatched actions against all reducers defined in [`reducers.js`](./reducers.js). To include a reducer in the global store, simply add the reducing function to the combined reducer exported by `reducers.js`. - -## Usage - -```js -import createReduxStore from 'lib/create-redux-store'; - -const store = createReduxStore(); -``` diff --git a/shared/lib/create-redux-store/index.js b/shared/lib/create-redux-store/index.js deleted file mode 100644 index 5277ef63a5e64..0000000000000 --- a/shared/lib/create-redux-store/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * External dependencies - */ -import thunkMiddleware from 'redux-thunk'; -import { createStore, applyMiddleware } from 'redux'; - -/** - * Internal dependencies - */ -import reducers from './reducers'; - -export default () => { - return applyMiddleware( - thunkMiddleware - )( createStore )( reducers ); -}; diff --git a/shared/lib/create-redux-store/reducers.js b/shared/lib/create-redux-store/reducers.js deleted file mode 100644 index 79e2e4ca2972b..0000000000000 --- a/shared/lib/create-redux-store/reducers.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * External dependencies - */ -import { combineReducers } from 'redux'; - -/** - * Internal dependencies - */ -import sharing from 'lib/sharing/reducers'; - -export default combineReducers( { - sharing -} ); From 75d3cf7da74082ea7e3bc9803c4b1cd81833a4cb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 7 Dec 2015 15:23:58 -0500 Subject: [PATCH 61/89] Sites: Split selected site management into UI Redux subtree --- client/state/index.js | 4 +- client/state/sites/README.md | 34 ++--------------- client/state/sites/action-types.js | 1 - client/state/sites/actions.js | 19 +--------- client/state/sites/reducers.js | 23 +----------- client/state/sites/test/actions.js | 21 +---------- client/state/sites/test/reducers.js | 27 +------------- client/state/ui/Makefile | 10 +++++ client/state/ui/README.md | 39 ++++++++++++++++++++ client/state/ui/action-types.js | 1 + client/state/ui/actions.js | 19 ++++++++++ client/state/ui/reducers.js | 30 +++++++++++++++ client/state/{sites => ui}/selectors.js | 6 +-- client/state/ui/test/actions.js | 23 ++++++++++++ client/state/ui/test/reducers.js | 29 +++++++++++++++ client/state/{sites => ui}/test/selectors.js | 8 ++-- 16 files changed, 170 insertions(+), 124 deletions(-) create mode 100644 client/state/ui/Makefile create mode 100644 client/state/ui/README.md create mode 100644 client/state/ui/action-types.js create mode 100644 client/state/ui/actions.js create mode 100644 client/state/ui/reducers.js rename client/state/{sites => ui}/selectors.js (71%) create mode 100644 client/state/ui/test/actions.js create mode 100644 client/state/ui/test/reducers.js rename client/state/{sites => ui}/test/selectors.js (89%) diff --git a/client/state/index.js b/client/state/index.js index bf6cea1aa5f25..d605154d942b4 100644 --- a/client/state/index.js +++ b/client/state/index.js @@ -9,13 +9,15 @@ import { createStore, applyMiddleware, combineReducers } from 'redux'; */ import sharing from './sharing/reducers'; import sites from './sites/reducers'; +import ui from './ui/reducers'; /** * Module variables */ const reducer = combineReducers( { sharing, - sites + sites, + ui } ); export function createReduxStore() { diff --git a/client/state/sites/README.md b/client/state/sites/README.md index d7cb042de564e..b70eeee519cc3 100644 --- a/client/state/sites/README.md +++ b/client/state/sites/README.md @@ -1,24 +1,14 @@ Sites ===== -A module for managing site data, including the sites themselves and related behaviors, most common of which is the currently selected site. +A module for managing site data. -__Note:__ This module does not yet have complete feature parity with [`sites-list`](../../../client/lib/sites-list). Refer to the set of actions and reducers below to determine whether your needs are satisfied. If not, consider adding support to the module, or use `sites-list` instead. +__Note:__ This module does not yet have complete feature parity with [`sites-list`](../../lib/sites-list). Refer to the set of actions and reducers below to determine whether your needs are satisfied. If not, consider adding support to the module, or use `sites-list` instead. ## Actions Used in combination with the Redux store instance `dispatch` function, actions can be used in manipulating the current global state. -### `setSelectedSite( siteId: Number )` - -Sets the currently selected site, by site ID. - -```js -import { setSelectedSite } from 'state/sites/actions'; - -dispatch( setSelectedSite( 2916284 ) ); -``` - ### `receiveSite( site: Object )` Adds a site object to the set of known sites. @@ -31,26 +21,8 @@ dispatch( receiveSite( { ID: 2916284, name: 'WordPress.com Example Blog' } ) ); ## Reducers -The Sites reducers add the following keys to the global state tree, under `sites`: - -#### `selected` - -The currently selected site, or `null` if no site is selected. +The included reducers add the following keys to the global state tree, under `sites`: #### `byId` All known sites, indexed by site ID. - -## Selectors - -Selectors are intended to assist in extracting data from the global state tree for consumption by other modules. - -#### `getSelectedSite( state: Object )` - -Returns the currently selected site object. - -```js -import { getSelectedSite } from 'state/sites/selectors'; - -const selectedSite = getSelectedSite( store.getState() ); -``` diff --git a/client/state/sites/action-types.js b/client/state/sites/action-types.js index 4a7b9ed52affd..25216cb6fb662 100644 --- a/client/state/sites/action-types.js +++ b/client/state/sites/action-types.js @@ -1,2 +1 @@ -export const SET_SELECTED_SITE = 'SET_SELECTED_SITE'; export const RECEIVE_SITE = 'RECEIVE_SITE'; diff --git a/client/state/sites/actions.js b/client/state/sites/actions.js index bea2f1c5dcf4a..e66a124c11bd3 100644 --- a/client/state/sites/actions.js +++ b/client/state/sites/actions.js @@ -1,24 +1,7 @@ /** * Internal dependencies */ -import { - SET_SELECTED_SITE, - RECEIVE_SITE -} from './action-types'; - -/** - * Returns an action object to be used in signalling that a site has been set - * as selected. - * - * @param {Number} siteId Site ID - * @return {Object} Action object - */ -export function setSelectedSite( siteId ) { - return { - type: SET_SELECTED_SITE, - siteId - }; -} +import { RECEIVE_SITE } from './action-types'; /** * Returns an action object to be used in signalling that a site object has diff --git a/client/state/sites/reducers.js b/client/state/sites/reducers.js index da1a13d0c9c09..6a1a0c3ea0421 100644 --- a/client/state/sites/reducers.js +++ b/client/state/sites/reducers.js @@ -6,27 +6,7 @@ import { combineReducers } from 'redux'; /** * Internal dependencies */ -import { - SET_SELECTED_SITE, - RECEIVE_SITE -} from './action-types'; - -/** - * Tracks the currently selected site ID. - * - * @param {Object} state Current state - * @param {Object} action Action payload - * @return {Object} Updated state - */ -export function selected( state = null, action ) { - switch ( action.type ) { - case SET_SELECTED_SITE: - state = action.siteId; - break; - } - - return state; -} +import { RECEIVE_SITE } from './action-types'; /** * Tracks all known site objects, indexed by site ID. @@ -48,6 +28,5 @@ export function byId( state = {}, action ) { } export default combineReducers( { - selected, byId } ); diff --git a/client/state/sites/test/actions.js b/client/state/sites/test/actions.js index e52f83e8db89c..66a9caa108630 100644 --- a/client/state/sites/test/actions.js +++ b/client/state/sites/test/actions.js @@ -6,27 +6,10 @@ import { expect } from 'chai'; /** * Internal dependencies */ -import { - SET_SELECTED_SITE, - RECEIVE_SITE -} from '../action-types'; -import { - setSelectedSite, - receiveSite -} from '../actions'; +import { RECEIVE_SITE } from '../action-types'; +import { receiveSite } from '../actions'; describe( 'actions', () => { - describe( '#setSelectedSite()', () => { - it( 'should return an action object', () => { - const action = setSelectedSite( 2916284 ); - - expect( action ).to.eql( { - type: SET_SELECTED_SITE, - siteId: 2916284 - } ); - } ); - } ); - describe( '#receiveSite()', () => { it( 'should return an action object', () => { const site = { ID: 2916284, name: 'WordPress.com Example Blog' }; diff --git a/client/state/sites/test/reducers.js b/client/state/sites/test/reducers.js index 7bd22fe37e471..3ab6771bf830e 100644 --- a/client/state/sites/test/reducers.js +++ b/client/state/sites/test/reducers.js @@ -6,33 +6,10 @@ import { expect } from 'chai'; /** * Internal dependencies */ -import { - SET_SELECTED_SITE, - RECEIVE_SITE -} from '../action-types'; -import { - selected, - byId -} from '../reducers'; +import { RECEIVE_SITE } from '../action-types'; +import { byId } from '../reducers'; describe( 'reducers', () => { - describe( '#selected()', () => { - it( 'should default to null', () => { - const state = selected( undefined, {} ); - - expect( state ).to.be.null; - } ); - - it( 'should set the selected site ID', () => { - const state = selected( null, { - type: SET_SELECTED_SITE, - siteId: 2916284 - } ); - - expect( state ).to.equal( 2916284 ); - } ); - } ); - describe( '#byId()', () => { it( 'should default to an empty object', () => { const state = byId( undefined, {} ); diff --git a/client/state/ui/Makefile b/client/state/ui/Makefile new file mode 100644 index 0000000000000..67b5631ee05af --- /dev/null +++ b/client/state/ui/Makefile @@ -0,0 +1,10 @@ +REPORTER ?= spec +NODE_BIN := $(shell npm bin) +MOCHA ?= $(NODE_BIN)/mocha +BASE_DIR := $(NODE_BIN)/../.. +NODE_PATH := $(BASE_DIR)/client:$(BASE_DIR)/shared + +test: + @NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers jsx:babel/register,js:babel/register --reporter $(REPORTER) + +.PHONY: test diff --git a/client/state/ui/README.md b/client/state/ui/README.md new file mode 100644 index 0000000000000..3e97cb723d726 --- /dev/null +++ b/client/state/ui/README.md @@ -0,0 +1,39 @@ +# UI + +A module for managing user interface state. + +## Actions + +Used in combination with the Redux store instance `dispatch` function, actions can be used in manipulating the current global state. + +### `setSelectedSite( siteId: Number )` + +Sets the currently selected site, by site ID. + +```js +import { setSelectedSite } from 'state/ui/actions'; + +dispatch( setSelectedSite( 2916284 ) ); +``` + +## Reducers + +The included reducers add the following keys to the global state tree, under `ui`: + +#### `selectedSite` + +The currently selected site, or `null` if no site is selected. + +## Selectors + +Selectors are intended to assist in extracting data from the global state tree for consumption by other modules. + +#### `getSelectedSite( state: Object )` + +Returns the currently selected site object. + +```js +import { getSelectedSite } from 'state/ui/selectors'; + +const selectedSite = getSelectedSite( store.getState() ); +``` diff --git a/client/state/ui/action-types.js b/client/state/ui/action-types.js new file mode 100644 index 0000000000000..03927409467b5 --- /dev/null +++ b/client/state/ui/action-types.js @@ -0,0 +1 @@ +export const SET_SELECTED_SITE = 'SET_SELECTED_SITE'; diff --git a/client/state/ui/actions.js b/client/state/ui/actions.js new file mode 100644 index 0000000000000..a6a7798528198 --- /dev/null +++ b/client/state/ui/actions.js @@ -0,0 +1,19 @@ +/** + * Internal dependencies + */ +import { SET_SELECTED_SITE } from './action-types'; + +/** + * Returns an action object to be used in signalling that a site has been set + * as selected. + * + * @param {Number} siteId Site ID + * @return {Object} Action object + */ +export function setSelectedSite( siteId ) { + return { + type: SET_SELECTED_SITE, + siteId + }; +} + diff --git a/client/state/ui/reducers.js b/client/state/ui/reducers.js new file mode 100644 index 0000000000000..60ef7a666ad34 --- /dev/null +++ b/client/state/ui/reducers.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { combineReducers } from 'redux'; + +/** + * Internal dependencies + */ +import { SET_SELECTED_SITE } from './action-types'; + +/** + * Tracks the currently selected site ID. + * + * @param {Object} state Current state + * @param {Object} action Action payload + * @return {Object} Updated state + */ +export function selectedSite( state = null, action ) { + switch ( action.type ) { + case SET_SELECTED_SITE: + state = action.siteId; + break; + } + + return state; +} + +export default combineReducers( { + selectedSite +} ); diff --git a/client/state/sites/selectors.js b/client/state/ui/selectors.js similarity index 71% rename from client/state/sites/selectors.js rename to client/state/ui/selectors.js index 183438b7a8efe..eab5ab09e207d 100644 --- a/client/state/sites/selectors.js +++ b/client/state/ui/selectors.js @@ -5,11 +5,9 @@ * @return {Object} Selected site */ export function getSelectedSite( state ) { - const { selected, byId } = state.sites; - - if ( ! selected ) { + if ( ! state.ui.selectedSite ) { return null; } - return byId[ selected ]; + return state.sites.byId[ state.ui.selectedSite ]; } diff --git a/client/state/ui/test/actions.js b/client/state/ui/test/actions.js new file mode 100644 index 0000000000000..276dcdbc607de --- /dev/null +++ b/client/state/ui/test/actions.js @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { SET_SELECTED_SITE } from '../action-types'; +import { setSelectedSite } from '../actions'; + +describe( 'actions', () => { + describe( '#setSelectedSite()', () => { + it( 'should return an action object', () => { + const action = setSelectedSite( 2916284 ); + + expect( action ).to.eql( { + type: SET_SELECTED_SITE, + siteId: 2916284 + } ); + } ); + } ); +} ); diff --git a/client/state/ui/test/reducers.js b/client/state/ui/test/reducers.js new file mode 100644 index 0000000000000..6860083e139e7 --- /dev/null +++ b/client/state/ui/test/reducers.js @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { SET_SELECTED_SITE } from '../action-types'; +import { selectedSite } from '../reducers'; + +describe( 'reducers', () => { + describe( '#selectedSite()', () => { + it( 'should default to null', () => { + const state = selectedSite( undefined, {} ); + + expect( state ).to.be.null; + } ); + + it( 'should set the selected site ID', () => { + const state = selectedSite( null, { + type: SET_SELECTED_SITE, + siteId: 2916284 + } ); + + expect( state ).to.equal( 2916284 ); + } ); + } ); +} ); diff --git a/client/state/sites/test/selectors.js b/client/state/ui/test/selectors.js similarity index 89% rename from client/state/sites/test/selectors.js rename to client/state/ui/test/selectors.js index 18e7183762217..9778d940b31ff 100644 --- a/client/state/sites/test/selectors.js +++ b/client/state/ui/test/selectors.js @@ -12,8 +12,8 @@ describe( 'selectors', () => { describe( '#getSelectedSite()', () => { it( 'should return null if no site is selected', () => { const selected = getSelectedSite( { - sites: { - selected: null + ui: { + selectedSite: null } } ); @@ -23,10 +23,12 @@ describe( 'selectors', () => { it( 'should return the object for the selected site', () => { const selected = getSelectedSite( { sites: { - selected: 2916284, byId: { 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } } + }, + ui: { + selectedSite: 2916284 } } ); From 8eb9519a927edd279dc69a34ed7683fe0e393438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Ventura?= Date: Tue, 8 Dec 2015 14:12:34 +0100 Subject: [PATCH 62/89] State: tweak some docs, add to client readme. --- client/README.md | 9 +++++++-- client/state/README.md | 6 +++++- client/state/ui/README.md | 3 ++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/client/README.md b/client/README.md index bf44ff8d8baca..33d9ac01359bf 100644 --- a/client/README.md +++ b/client/README.md @@ -16,16 +16,21 @@ These are some of the key modules of the application, kept in `client`'s root fo The `/components` folder holds all the internal React components used to build the Calypso UI across sections. Most of these are rendered in `/devdocs/design` for reference. +### State + +The `/state` folder holds the structure of the application global state and data flows. + ### Lib The `/lib` folder holds internal modules and utilities that power Calypso. ### Core Sections -These represent the top section in the masterbar and handle the controllers for the entire app. Most React components live within their specific section. +These represent the top section in the masterbar as well as other significant areas of the app. Within these all the controllers for the entire app are expressed. Most React app-components live within their specific section and not in `client/components`. * `my-sites`: the site related admin functionality. Akin to wp-admin. * `reader`: the home of all Reader sections. * `notifications`: the notifications panel. * `me`: the sections under the `/me` route. - +* `post-editor`: the editor. +* `signup`: the signup flows. diff --git a/client/state/README.md b/client/state/README.md index d81a1193fa9f2..423d73aae3945 100644 --- a/client/state/README.md +++ b/client/state/README.md @@ -1,7 +1,7 @@ State ===== -This directory contains all of the behavior describing the global application state. Folders within this directory reflect sub-trees within the global state tree, each with their own set of actions, reducers, and utilities. +This directory contains all of the behavior describing the global application state. Folders within this directory reflect sub-trees of the global state tree, each with their own set of actions, reducers, and selectors. The root module exports a single function which, when invoked, returns an instance of a [Redux store](http://redux.js.org/docs/basics/Store.html). The store instance runs dispatched actions against all known reducers. To include a reducer in the global store, simply add the reducing function to the combined reducer in `index.js`. @@ -12,3 +12,7 @@ import { createReduxStore } from 'state'; const store = createReduxStore(); ``` + +## Adding to the tree + +All the application information and data in Calypso should go through this data flow. When you are creating a new module with new data requirements, you should add them to this global store. diff --git a/client/state/ui/README.md b/client/state/ui/README.md index 3e97cb723d726..a05eb5df1b75c 100644 --- a/client/state/ui/README.md +++ b/client/state/ui/README.md @@ -1,4 +1,5 @@ -# UI +UI State +======== A module for managing user interface state. From dd58b14d840f71a749ca8f0a1e15d17f06291369 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 8 Dec 2015 10:12:42 -0500 Subject: [PATCH 63/89] Sites: Rename site mapping from byId to items # Conflicts: # client/components/drop-zone/index.jsx --- client/state/sites/README.md | 2 +- client/state/sites/reducers.js | 4 ++-- client/state/sites/test/reducers.js | 14 +++++++------- client/state/ui/selectors.js | 2 +- client/state/ui/test/selectors.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/client/state/sites/README.md b/client/state/sites/README.md index b70eeee519cc3..604827e4a1a67 100644 --- a/client/state/sites/README.md +++ b/client/state/sites/README.md @@ -23,6 +23,6 @@ dispatch( receiveSite( { ID: 2916284, name: 'WordPress.com Example Blog' } ) ); The included reducers add the following keys to the global state tree, under `sites`: -#### `byId` +#### `items` All known sites, indexed by site ID. diff --git a/client/state/sites/reducers.js b/client/state/sites/reducers.js index 6a1a0c3ea0421..25617a915c92c 100644 --- a/client/state/sites/reducers.js +++ b/client/state/sites/reducers.js @@ -15,7 +15,7 @@ import { RECEIVE_SITE } from './action-types'; * @param {Object} action Action payload * @return {Object} Updated state */ -export function byId( state = {}, action ) { +export function items( state = {}, action ) { switch ( action.type ) { case RECEIVE_SITE: state = Object.assign( {}, state, { @@ -28,5 +28,5 @@ export function byId( state = {}, action ) { } export default combineReducers( { - byId + items } ); diff --git a/client/state/sites/test/reducers.js b/client/state/sites/test/reducers.js index 3ab6771bf830e..e7fa2f0394072 100644 --- a/client/state/sites/test/reducers.js +++ b/client/state/sites/test/reducers.js @@ -7,18 +7,18 @@ import { expect } from 'chai'; * Internal dependencies */ import { RECEIVE_SITE } from '../action-types'; -import { byId } from '../reducers'; +import { items } from '../reducers'; describe( 'reducers', () => { - describe( '#byId()', () => { + describe( '#items()', () => { it( 'should default to an empty object', () => { - const state = byId( undefined, {} ); + const state = items( undefined, {} ); expect( state ).to.eql( {} ); } ); it( 'should index sites by ID', () => { - const state = byId( null, { + const state = items( null, { type: RECEIVE_SITE, site: { ID: 2916284, name: 'WordPress.com Example Blog' } } ); @@ -30,7 +30,7 @@ describe( 'reducers', () => { it( 'should return a new instance of the state object', () => { const original = {}; - const state = byId( original, { + const state = items( original, { type: RECEIVE_SITE, site: { ID: 2916284, name: 'WordPress.com Example Blog' } } ); @@ -39,7 +39,7 @@ describe( 'reducers', () => { } ); it( 'should accumulate sites', () => { - const state = byId( { + const state = items( { 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } }, { type: RECEIVE_SITE, @@ -53,7 +53,7 @@ describe( 'reducers', () => { } ); it( 'should override previous site of same ID', () => { - const state = byId( { + const state = items( { 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } }, { type: RECEIVE_SITE, diff --git a/client/state/ui/selectors.js b/client/state/ui/selectors.js index eab5ab09e207d..8de0e69333811 100644 --- a/client/state/ui/selectors.js +++ b/client/state/ui/selectors.js @@ -9,5 +9,5 @@ export function getSelectedSite( state ) { return null; } - return state.sites.byId[ state.ui.selectedSite ]; + return state.sites.items[ state.ui.selectedSite ]; } diff --git a/client/state/ui/test/selectors.js b/client/state/ui/test/selectors.js index 9778d940b31ff..16ffb2452e050 100644 --- a/client/state/ui/test/selectors.js +++ b/client/state/ui/test/selectors.js @@ -23,7 +23,7 @@ describe( 'selectors', () => { it( 'should return the object for the selected site', () => { const selected = getSelectedSite( { sites: { - byId: { + items: { 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } } }, From 786005fb4384d6be618fa528af61a8fdc862a579 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 8 Dec 2015 15:12:16 -0500 Subject: [PATCH 64/89] Framework: Rename state reducers as reducer Accurately reflect that a single reducer function is exported, though an aggregate of sub-keys within that segment of the state tree --- client/state/README.md | 2 +- client/state/index.js | 6 +++--- client/state/sharing/publicize/{reducers.js => reducer.js} | 0 .../sharing/publicize/test/{reducers.js => reducer.js} | 2 +- client/state/sharing/{reducers.js => reducer.js} | 2 +- client/state/sites/{reducers.js => reducer.js} | 0 client/state/sites/test/{reducers.js => reducer.js} | 4 ++-- client/state/ui/{reducers.js => reducer.js} | 0 client/state/ui/test/{reducers.js => reducer.js} | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) rename client/state/sharing/publicize/{reducers.js => reducer.js} (100%) rename client/state/sharing/publicize/test/{reducers.js => reducer.js} (99%) rename client/state/sharing/{reducers.js => reducer.js} (77%) rename client/state/sites/{reducers.js => reducer.js} (100%) rename client/state/sites/test/{reducers.js => reducer.js} (95%) rename client/state/ui/{reducers.js => reducer.js} (100%) rename client/state/ui/test/{reducers.js => reducer.js} (87%) diff --git a/client/state/README.md b/client/state/README.md index 423d73aae3945..7c18f89842cf1 100644 --- a/client/state/README.md +++ b/client/state/README.md @@ -1,7 +1,7 @@ State ===== -This directory contains all of the behavior describing the global application state. Folders within this directory reflect sub-trees of the global state tree, each with their own set of actions, reducers, and selectors. +This directory contains all of the behavior describing the global application state. Folders within this directory reflect sub-trees of the global state tree, each with their own reducer, actions, and selectors. The root module exports a single function which, when invoked, returns an instance of a [Redux store](http://redux.js.org/docs/basics/Store.html). The store instance runs dispatched actions against all known reducers. To include a reducer in the global store, simply add the reducing function to the combined reducer in `index.js`. diff --git a/client/state/index.js b/client/state/index.js index d605154d942b4..52cbab6b5a1f7 100644 --- a/client/state/index.js +++ b/client/state/index.js @@ -7,9 +7,9 @@ import { createStore, applyMiddleware, combineReducers } from 'redux'; /** * Internal dependencies */ -import sharing from './sharing/reducers'; -import sites from './sites/reducers'; -import ui from './ui/reducers'; +import sharing from './sharing/reducer'; +import sites from './sites/reducer'; +import ui from './ui/reducer'; /** * Module variables diff --git a/client/state/sharing/publicize/reducers.js b/client/state/sharing/publicize/reducer.js similarity index 100% rename from client/state/sharing/publicize/reducers.js rename to client/state/sharing/publicize/reducer.js diff --git a/client/state/sharing/publicize/test/reducers.js b/client/state/sharing/publicize/test/reducer.js similarity index 99% rename from client/state/sharing/publicize/test/reducers.js rename to client/state/sharing/publicize/test/reducer.js index 7dcd22c05da89..d6188fc4b9dc0 100644 --- a/client/state/sharing/publicize/test/reducers.js +++ b/client/state/sharing/publicize/test/reducer.js @@ -15,7 +15,7 @@ import { fetchingConnections, connections, connectionsBySiteId -} from '../reducers'; +} from '../reducer'; describe( '#fetchingConnections()', () => { it( 'should set fetching to true for fetching action', () => { diff --git a/client/state/sharing/reducers.js b/client/state/sharing/reducer.js similarity index 77% rename from client/state/sharing/reducers.js rename to client/state/sharing/reducer.js index 8de76c1d16579..41eb4164358a0 100644 --- a/client/state/sharing/reducers.js +++ b/client/state/sharing/reducer.js @@ -6,7 +6,7 @@ import { combineReducers } from 'redux'; /** * Internal dependencies */ -import publicize from './publicize/reducers'; +import publicize from './publicize/reducer'; export default combineReducers( { publicize diff --git a/client/state/sites/reducers.js b/client/state/sites/reducer.js similarity index 100% rename from client/state/sites/reducers.js rename to client/state/sites/reducer.js diff --git a/client/state/sites/test/reducers.js b/client/state/sites/test/reducer.js similarity index 95% rename from client/state/sites/test/reducers.js rename to client/state/sites/test/reducer.js index e7fa2f0394072..fd6dead8d3ab4 100644 --- a/client/state/sites/test/reducers.js +++ b/client/state/sites/test/reducer.js @@ -7,9 +7,9 @@ import { expect } from 'chai'; * Internal dependencies */ import { RECEIVE_SITE } from '../action-types'; -import { items } from '../reducers'; +import { items } from '../reducer'; -describe( 'reducers', () => { +describe( 'reducer', () => { describe( '#items()', () => { it( 'should default to an empty object', () => { const state = items( undefined, {} ); diff --git a/client/state/ui/reducers.js b/client/state/ui/reducer.js similarity index 100% rename from client/state/ui/reducers.js rename to client/state/ui/reducer.js diff --git a/client/state/ui/test/reducers.js b/client/state/ui/test/reducer.js similarity index 87% rename from client/state/ui/test/reducers.js rename to client/state/ui/test/reducer.js index 6860083e139e7..b754a02ef6ad2 100644 --- a/client/state/ui/test/reducers.js +++ b/client/state/ui/test/reducer.js @@ -7,9 +7,9 @@ import { expect } from 'chai'; * Internal dependencies */ import { SET_SELECTED_SITE } from '../action-types'; -import { selectedSite } from '../reducers'; +import { selectedSite } from '../reducer'; -describe( 'reducers', () => { +describe( 'reducer', () => { describe( '#selectedSite()', () => { it( 'should default to null', () => { const state = selectedSite( undefined, {} ); From bb769386410b500daeb3bbf96ea973c4b056154c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 8 Dec 2015 15:39:29 -0500 Subject: [PATCH 65/89] Sites: Always freeze original state value Thus eliminating the need for a separate test on new instance --- client/state/sites/test/reducer.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/client/state/sites/test/reducer.js b/client/state/sites/test/reducer.js index fd6dead8d3ab4..f80af13443edb 100644 --- a/client/state/sites/test/reducer.js +++ b/client/state/sites/test/reducer.js @@ -28,20 +28,11 @@ describe( 'reducer', () => { } ); } ); - it( 'should return a new instance of the state object', () => { - const original = {}; - const state = items( original, { - type: RECEIVE_SITE, - site: { ID: 2916284, name: 'WordPress.com Example Blog' } - } ); - - expect( state ).to.not.equal( original ); - } ); - it( 'should accumulate sites', () => { - const state = items( { + const original = Object.freeze( { 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } - }, { + } ); + const state = items( original, { type: RECEIVE_SITE, site: { ID: 77203074, name: 'Just You Wait' } } ); @@ -53,9 +44,10 @@ describe( 'reducer', () => { } ); it( 'should override previous site of same ID', () => { - const state = items( { + const original = Object.freeze( { 2916284: { ID: 2916284, name: 'WordPress.com Example Blog' } - }, { + } ); + const state = items( original, { type: RECEIVE_SITE, site: { ID: 2916284, name: 'Just You Wait' } } ); From e1f2de296253b1b621d0fe06a654d0cde7dff1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Ventura?= Date: Wed, 9 Dec 2015 11:17:31 +0100 Subject: [PATCH 66/89] Expose redux store as context.store for simplicity. --- client/boot/index.js | 2 +- client/post-editor/controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/boot/index.js b/client/boot/index.js index 8239b2ec9a174..22a432d256eda 100644 --- a/client/boot/index.js +++ b/client/boot/index.js @@ -87,7 +87,7 @@ function setUpContext( layout ) { var parsed = url.parse( location.href, true ); context.layout = layout; - context.reduxStore = reduxStore; + context.store = reduxStore; // Break routing and do full page load for logout link in /me if ( context.pathname === '/wp-login.php' ) { diff --git a/client/post-editor/controller.js b/client/post-editor/controller.js index 16aa24d5fe9c6..b4fd7b922677a 100644 --- a/client/post-editor/controller.js +++ b/client/post-editor/controller.js @@ -38,7 +38,7 @@ function renderEditor( context, postType ) { React.unmountComponentAtNode( document.getElementById( 'secondary' ) ); React.render( - React.createElement( ReduxProvider, { store: context.reduxStore }, () => { + React.createElement( ReduxProvider, { store: context.store }, () => { return React.createElement( PreferencesData, null, React.createElement( PostEditor, { sites: sites, From f161859aae748632b07f9f965f6ab55cee3cf21d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 10 Dec 2015 13:09:33 -0500 Subject: [PATCH 67/89] Framework: Migrate action types to single global file --- client/state/action-types.js | 12 ++++++++++++ client/state/sharing/action-types.js | 3 --- client/state/sharing/publicize/actions.js | 2 +- client/state/sharing/publicize/reducer.js | 2 +- client/state/sharing/publicize/test/actions.js | 2 +- client/state/sharing/publicize/test/reducer.js | 2 +- client/state/sites/action-types.js | 1 - client/state/sites/actions.js | 2 +- client/state/sites/reducer.js | 2 +- client/state/sites/test/actions.js | 2 +- client/state/sites/test/reducer.js | 2 +- client/state/ui/action-types.js | 1 - client/state/ui/actions.js | 2 +- client/state/ui/reducer.js | 2 +- client/state/ui/test/actions.js | 2 +- client/state/ui/test/reducer.js | 2 +- 16 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 client/state/action-types.js delete mode 100644 client/state/sharing/action-types.js delete mode 100644 client/state/sites/action-types.js delete mode 100644 client/state/ui/action-types.js diff --git a/client/state/action-types.js b/client/state/action-types.js new file mode 100644 index 0000000000000..68b678dae1d51 --- /dev/null +++ b/client/state/action-types.js @@ -0,0 +1,12 @@ +/** + * Any new action type should be added to the set of exports below, with the + * value mirroring its exported name. + * + * Please keep this list alphabetized! + */ + +export const FAIL_PUBLICIZE_CONNECTIONS_REQUEST = 'FAIL_PUBLICIZE_CONNECTIONS_REQUEST'; +export const FETCH_PUBLICIZE_CONNECTIONS = 'FETCH_PUBLICIZE_CONNECTIONS'; +export const RECEIVE_PUBLICIZE_CONNECTIONS = 'RECEIVE_PUBLICIZE_CONNECTIONS'; +export const RECEIVE_SITE = 'RECEIVE_SITE'; +export const SET_SELECTED_SITE = 'SET_SELECTED_SITE'; diff --git a/client/state/sharing/action-types.js b/client/state/sharing/action-types.js deleted file mode 100644 index 7163516c2d278..0000000000000 --- a/client/state/sharing/action-types.js +++ /dev/null @@ -1,3 +0,0 @@ -export const FETCH_PUBLICIZE_CONNECTIONS = 'FETCH_PUBLICIZE_CONNECTIONS'; -export const RECEIVE_PUBLICIZE_CONNECTIONS = 'RECEIVE_PUBLICIZE_CONNECTIONS'; -export const FAIL_PUBLICIZE_CONNECTIONS_REQUEST = 'FAIL_PUBLICIZE_CONNECTIONS_REQUEST'; diff --git a/client/state/sharing/publicize/actions.js b/client/state/sharing/publicize/actions.js index 15d74b76b5b0d..d8cd9abd70416 100644 --- a/client/state/sharing/publicize/actions.js +++ b/client/state/sharing/publicize/actions.js @@ -6,7 +6,7 @@ import { FETCH_PUBLICIZE_CONNECTIONS, RECEIVE_PUBLICIZE_CONNECTIONS, FAIL_PUBLICIZE_CONNECTIONS_REQUEST -} from '../action-types'; +} from 'state/action-types'; /** * Triggers a network request to fetch Publicize connections for the specified diff --git a/client/state/sharing/publicize/reducer.js b/client/state/sharing/publicize/reducer.js index 0956980cbc3c8..cba5f4fca4605 100644 --- a/client/state/sharing/publicize/reducer.js +++ b/client/state/sharing/publicize/reducer.js @@ -11,7 +11,7 @@ import { FETCH_PUBLICIZE_CONNECTIONS, RECEIVE_PUBLICIZE_CONNECTIONS, FAIL_PUBLICIZE_CONNECTIONS_REQUEST -} from '../action-types'; +} from 'state/action-types'; /** * Track the current status for fetching connections. Maps site ID to the diff --git a/client/state/sharing/publicize/test/actions.js b/client/state/sharing/publicize/test/actions.js index 0d23c6ecdbc1b..c85e6d3c4aa7f 100644 --- a/client/state/sharing/publicize/test/actions.js +++ b/client/state/sharing/publicize/test/actions.js @@ -13,7 +13,7 @@ import { FETCH_PUBLICIZE_CONNECTIONS, RECEIVE_PUBLICIZE_CONNECTIONS, FAIL_PUBLICIZE_CONNECTIONS_REQUEST -} from '../../action-types'; +} from 'state/action-types'; import { fetchConnections, receiveConnections, diff --git a/client/state/sharing/publicize/test/reducer.js b/client/state/sharing/publicize/test/reducer.js index d6188fc4b9dc0..bd888279519f6 100644 --- a/client/state/sharing/publicize/test/reducer.js +++ b/client/state/sharing/publicize/test/reducer.js @@ -10,7 +10,7 @@ import { FETCH_PUBLICIZE_CONNECTIONS, RECEIVE_PUBLICIZE_CONNECTIONS, FAIL_PUBLICIZE_CONNECTIONS_REQUEST -} from '../../action-types'; +} from 'state/action-types'; import { fetchingConnections, connections, diff --git a/client/state/sites/action-types.js b/client/state/sites/action-types.js deleted file mode 100644 index 25216cb6fb662..0000000000000 --- a/client/state/sites/action-types.js +++ /dev/null @@ -1 +0,0 @@ -export const RECEIVE_SITE = 'RECEIVE_SITE'; diff --git a/client/state/sites/actions.js b/client/state/sites/actions.js index e66a124c11bd3..e0d36ac887f37 100644 --- a/client/state/sites/actions.js +++ b/client/state/sites/actions.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { RECEIVE_SITE } from './action-types'; +import { RECEIVE_SITE } from 'state/action-types'; /** * Returns an action object to be used in signalling that a site object has diff --git a/client/state/sites/reducer.js b/client/state/sites/reducer.js index 25617a915c92c..6ce87816bbb65 100644 --- a/client/state/sites/reducer.js +++ b/client/state/sites/reducer.js @@ -6,7 +6,7 @@ import { combineReducers } from 'redux'; /** * Internal dependencies */ -import { RECEIVE_SITE } from './action-types'; +import { RECEIVE_SITE } from 'state/action-types'; /** * Tracks all known site objects, indexed by site ID. diff --git a/client/state/sites/test/actions.js b/client/state/sites/test/actions.js index 66a9caa108630..e6783cca988b3 100644 --- a/client/state/sites/test/actions.js +++ b/client/state/sites/test/actions.js @@ -6,7 +6,7 @@ import { expect } from 'chai'; /** * Internal dependencies */ -import { RECEIVE_SITE } from '../action-types'; +import { RECEIVE_SITE } from 'state/action-types'; import { receiveSite } from '../actions'; describe( 'actions', () => { diff --git a/client/state/sites/test/reducer.js b/client/state/sites/test/reducer.js index f80af13443edb..3ec131939ab5a 100644 --- a/client/state/sites/test/reducer.js +++ b/client/state/sites/test/reducer.js @@ -6,7 +6,7 @@ import { expect } from 'chai'; /** * Internal dependencies */ -import { RECEIVE_SITE } from '../action-types'; +import { RECEIVE_SITE } from 'state/action-types'; import { items } from '../reducer'; describe( 'reducer', () => { diff --git a/client/state/ui/action-types.js b/client/state/ui/action-types.js deleted file mode 100644 index 03927409467b5..0000000000000 --- a/client/state/ui/action-types.js +++ /dev/null @@ -1 +0,0 @@ -export const SET_SELECTED_SITE = 'SET_SELECTED_SITE'; diff --git a/client/state/ui/actions.js b/client/state/ui/actions.js index a6a7798528198..b9da18c3649fb 100644 --- a/client/state/ui/actions.js +++ b/client/state/ui/actions.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { SET_SELECTED_SITE } from './action-types'; +import { SET_SELECTED_SITE } from 'state/action-types'; /** * Returns an action object to be used in signalling that a site has been set diff --git a/client/state/ui/reducer.js b/client/state/ui/reducer.js index 60ef7a666ad34..00c570f9acb4a 100644 --- a/client/state/ui/reducer.js +++ b/client/state/ui/reducer.js @@ -6,7 +6,7 @@ import { combineReducers } from 'redux'; /** * Internal dependencies */ -import { SET_SELECTED_SITE } from './action-types'; +import { SET_SELECTED_SITE } from 'state/action-types'; /** * Tracks the currently selected site ID. diff --git a/client/state/ui/test/actions.js b/client/state/ui/test/actions.js index 276dcdbc607de..3eed1fd27a7eb 100644 --- a/client/state/ui/test/actions.js +++ b/client/state/ui/test/actions.js @@ -6,7 +6,7 @@ import { expect } from 'chai'; /** * Internal dependencies */ -import { SET_SELECTED_SITE } from '../action-types'; +import { SET_SELECTED_SITE } from 'state/action-types'; import { setSelectedSite } from '../actions'; describe( 'actions', () => { diff --git a/client/state/ui/test/reducer.js b/client/state/ui/test/reducer.js index b754a02ef6ad2..7058d6882a115 100644 --- a/client/state/ui/test/reducer.js +++ b/client/state/ui/test/reducer.js @@ -6,7 +6,7 @@ import { expect } from 'chai'; /** * Internal dependencies */ -import { SET_SELECTED_SITE } from '../action-types'; +import { SET_SELECTED_SITE } from 'state/action-types'; import { selectedSite } from '../reducer'; describe( 'reducer', () => { From b075587d8905aa398f125d1430f7a1589881e93b Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Wed, 2 Dec 2015 14:14:50 +0100 Subject: [PATCH 68/89] Business Plugins: Adds a feature example to the empty content view --- .../jetpack-manage-error-page/index.jsx | 5 +- client/my-sites/plugins/access-control.js | 46 ++++++++++++++++++- .../my-sites/plugins/plugin-item/style.scss | 2 + shared/components/empty-content/README.md | 2 +- .../empty-content/empty-content.jsx | 28 +++++++---- 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/client/my-sites/jetpack-manage-error-page/index.jsx b/client/my-sites/jetpack-manage-error-page/index.jsx index a3a90c5af0779..9c2181d5d81a1 100644 --- a/client/my-sites/jetpack-manage-error-page/index.jsx +++ b/client/my-sites/jetpack-manage-error-page/index.jsx @@ -69,15 +69,12 @@ module.exports = React.createClass( { if ( this.actionCallbacks[ this.props.template ] ) { settings.actionCallback = this[ this.actionCallbacks[ this.props.template ] ]; } + settings.featureExample = this.props.featureExample; const emptyContent = React.createElement( EmptyContent, settings ); - const featureExample = this.props.featureExample - ? { this.props.featureExample } - : null; return (
    { emptyContent } - { featureExample }
    ) } diff --git a/client/my-sites/plugins/access-control.js b/client/my-sites/plugins/access-control.js index 0eadb268703d1..efc9aa9bcb33f 100644 --- a/client/my-sites/plugins/access-control.js +++ b/client/my-sites/plugins/access-control.js @@ -1,7 +1,13 @@ +/** + * External Dependencies + */ +var React = require( 'react' ); + /** * Internal Dependencies */ -var sites = require( 'lib/sites-list' )(), + var PluginItem = require( 'my-sites/plugins/plugin-item/plugin-item' ), + sites = require( 'lib/sites-list' )(), i18n = require( 'lib/mixins/i18n' ), config = require( 'config' ), analytics = require( 'analytics' ), @@ -18,6 +24,41 @@ function hasErrorCondition( site, type ) { return errorConditions[ type ]; } +function getMockBusinessPluginItems() { + const plugins = [ { + slug: 'ecwid', + name: 'Ecwid', + wpcom: true, + icon: '/calypso/images/upgrades/plugins/ecwid.png' + }, { + slug: 'gumroad', + name: 'Gumroad', + wpcom: true, + icon: '/calypso/images/upgrades/plugins/gumroad.png' + }, { + slug: 'shopify', + name: 'Shopify', + wpcom: true, + icon: '/calypso/images/upgrades/plugins/gumroad.png' + } ]; + const selectedSite = { + slug: 'no-slug', + canUpdateFiles: true, + name: 'Not a real site' + } + + return plugins.map( plugin => { + return React.createElement( PluginItem, { + key: 'plugin-item-mock-' + plugin.slug, + plugin: plugin, + sites: [], + selectedSite: selectedSite, + progress: [], + isMock: true } + ); + } ); +} + function hasRestrictedAccess( site ) { var pluginPageError; @@ -56,7 +97,8 @@ function hasRestrictedAccess( site ) { illustration: '/calypso/images/drake/drake-whoops.svg', actionCallback: function() { analytics.tracks.recordEvent( 'calypso_upgrade_nudge_cta_click', { cta_name: 'business_plugins' } ); - } + }, + featureExample: getMockBusinessPluginItems() }; } diff --git a/client/my-sites/plugins/plugin-item/style.scss b/client/my-sites/plugins/plugin-item/style.scss index 819673bcbc50d..c038d580cd810 100644 --- a/client/my-sites/plugins/plugin-item/style.scss +++ b/client/my-sites/plugins/plugin-item/style.scss @@ -92,6 +92,7 @@ font-size: 14px; font-weight: 600; white-space: pre; + text-align: left; text-overflow: ellipsis; overflow: hidden; @@ -140,6 +141,7 @@ font-size: 11px; color: $gray; white-space: pre; + text-align: left; text-overflow: ellipsis; overflow: hidden; diff --git a/shared/components/empty-content/README.md b/shared/components/empty-content/README.md index fc972fbad309c..866c3f7daf785 100644 --- a/shared/components/empty-content/README.md +++ b/shared/components/empty-content/README.md @@ -31,7 +31,6 @@ render: function() { ); } - ``` ## Properties @@ -44,6 +43,7 @@ render: function() { * actionURL — (optional) `href` value for the primary action button. * actionCallback — (optional) `onClick` value for the primary action button. * actionTarget - (optional) If ommitted, no target attribute is specified. +* featureExample - (optional) If you initialize the component with this property, containing jsx, it will be added in the bottom of the Empty Content component as an example of what the user could be viewing if there were any content. The component also supports a secondary action. This should be used sparingly. diff --git a/shared/components/empty-content/empty-content.jsx b/shared/components/empty-content/empty-content.jsx index 23d6e0d94af27..3e0f17ac7d007 100644 --- a/shared/components/empty-content/empty-content.jsx +++ b/shared/components/empty-content/empty-content.jsx @@ -5,6 +5,11 @@ var React = require( 'react' ), debug = require( 'debug' )( 'calypso:components:emptyContent' ), classNames = require( 'classnames' ); +/** + * Internal dependencies + */ +var FeatureExample = require( 'components/feature-example' ); + module.exports = React.createClass( { displayName: 'EmptyContent', @@ -27,7 +32,8 @@ module.exports = React.createClass( { secondaryActionURL: React.PropTypes.string, secondaryActionCallback: React.PropTypes.func, className: React.PropTypes.string, - isCompact: React.PropTypes.bool + isCompact: React.PropTypes.bool, + featureExample: React.PropTypes.element }, componentDidMount: function() { @@ -81,7 +87,7 @@ module.exports = React.createClass( { }, render: function() { - var action, secondaryAction, illustration; + var action, secondaryAction, illustration, featureExample; if ( this.props.action ) { action = this.primaryAction(); @@ -95,25 +101,31 @@ module.exports = React.createClass( { illustration = ; } + if ( this.props.featureExample ) { + featureExample = { this.props.featureExample }; + } + return (
    { illustration } { - this.props.title ? -

    { this.props.title }

    : - null + this.props.title + ?

    { this.props.title }

    + : null } { - this.props.line ? -

    { this.props.line }

    : - null + this.props.line + ?

    { this.props.line }

    + : null } { action } { secondaryAction } + + { featureExample }
    ); } From afd637a164b922cc97d894d79478798e95e9b43d Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Mon, 7 Dec 2015 13:24:33 +0100 Subject: [PATCH 69/89] Empty-Content: Extract feature-example to be called from the parent component instead of doing it as part of empty-content --- .../my-sites/jetpack-manage-error-page/index.jsx | 5 ++++- client/my-sites/plugins/main.jsx | 2 ++ client/my-sites/plugins/plugin.jsx | 8 +++++++- shared/components/empty-content/README.md | 1 - .../components/empty-content/empty-content.jsx | 16 ++-------------- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/client/my-sites/jetpack-manage-error-page/index.jsx b/client/my-sites/jetpack-manage-error-page/index.jsx index 9c2181d5d81a1..a3a90c5af0779 100644 --- a/client/my-sites/jetpack-manage-error-page/index.jsx +++ b/client/my-sites/jetpack-manage-error-page/index.jsx @@ -69,12 +69,15 @@ module.exports = React.createClass( { if ( this.actionCallbacks[ this.props.template ] ) { settings.actionCallback = this[ this.actionCallbacks[ this.props.template ] ]; } - settings.featureExample = this.props.featureExample; const emptyContent = React.createElement( EmptyContent, settings ); + const featureExample = this.props.featureExample + ? { this.props.featureExample } + : null; return (
    { emptyContent } + { featureExample }
    ) } diff --git a/client/my-sites/plugins/main.jsx b/client/my-sites/plugins/main.jsx index a8de24f7530e5..61edc5354acc5 100644 --- a/client/my-sites/plugins/main.jsx +++ b/client/my-sites/plugins/main.jsx @@ -40,6 +40,7 @@ import PluginsDataStore from 'lib/plugins/wporg-data/store' import PluginNotices from 'lib/plugins/notices' import JetpackManageErrorPage from 'my-sites/jetpack-manage-error-page' import PlanNudge from 'components/plans/plan-nudge' +import FeatureExample from 'components/feature-example' /** * Module variables @@ -703,6 +704,7 @@ export default React.createClass( { return (
    + { this.state.accessError.featureExample ? { this.state.accessError.featureExample } : null }
    ); } diff --git a/client/my-sites/plugins/plugin.jsx b/client/my-sites/plugins/plugin.jsx index 5ffbc0ac44213..7156ee79cad8a 100644 --- a/client/my-sites/plugins/plugin.jsx +++ b/client/my-sites/plugins/plugin.jsx @@ -29,6 +29,7 @@ import JetpackManageErrorPage from 'my-sites/jetpack-manage-error-page'; import PluginSections from 'my-sites/plugins/plugin-sections'; import pluginsAccessControl from 'my-sites/plugins/access-control'; import EmptyContent from 'components/empty-content'; +import FeatureExample from 'components/feature-example' /** * Module variables @@ -286,7 +287,12 @@ export default React.createClass( { } if ( this.state.accessError ) { - return ; + return ( + + + { this.state.accessError.featureExample ? { this.state.accessError.featureExample } : null } + + ); } if ( this.state.isFetching ) { diff --git a/shared/components/empty-content/README.md b/shared/components/empty-content/README.md index 866c3f7daf785..1ffe23e89ee47 100644 --- a/shared/components/empty-content/README.md +++ b/shared/components/empty-content/README.md @@ -43,7 +43,6 @@ render: function() { * actionURL — (optional) `href` value for the primary action button. * actionCallback — (optional) `onClick` value for the primary action button. * actionTarget - (optional) If ommitted, no target attribute is specified. -* featureExample - (optional) If you initialize the component with this property, containing jsx, it will be added in the bottom of the Empty Content component as an example of what the user could be viewing if there were any content. The component also supports a secondary action. This should be used sparingly. diff --git a/shared/components/empty-content/empty-content.jsx b/shared/components/empty-content/empty-content.jsx index 3e0f17ac7d007..14e76cc578680 100644 --- a/shared/components/empty-content/empty-content.jsx +++ b/shared/components/empty-content/empty-content.jsx @@ -5,11 +5,6 @@ var React = require( 'react' ), debug = require( 'debug' )( 'calypso:components:emptyContent' ), classNames = require( 'classnames' ); -/** - * Internal dependencies - */ -var FeatureExample = require( 'components/feature-example' ); - module.exports = React.createClass( { displayName: 'EmptyContent', @@ -32,8 +27,7 @@ module.exports = React.createClass( { secondaryActionURL: React.PropTypes.string, secondaryActionCallback: React.PropTypes.func, className: React.PropTypes.string, - isCompact: React.PropTypes.bool, - featureExample: React.PropTypes.element + isCompact: React.PropTypes.bool }, componentDidMount: function() { @@ -87,7 +81,7 @@ module.exports = React.createClass( { }, render: function() { - var action, secondaryAction, illustration, featureExample; + var action, secondaryAction, illustration; if ( this.props.action ) { action = this.primaryAction(); @@ -101,10 +95,6 @@ module.exports = React.createClass( { illustration = ; } - if ( this.props.featureExample ) { - featureExample = { this.props.featureExample }; - } - return (
    { illustration } @@ -124,8 +114,6 @@ module.exports = React.createClass( { { action } { secondaryAction } - - { featureExample }
    ); } From 4057070cbcbfb9f2c87f76dd6219428d82c77a61 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Thu, 10 Dec 2015 17:56:29 +0100 Subject: [PATCH 70/89] Plugins: Convert access-control to ES6 --- client/my-sites/plugins/access-control.js | 36 ++++++++++--------- .../plugins/plugins-browser-list/index.jsx | 18 +++++----- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/client/my-sites/plugins/access-control.js b/client/my-sites/plugins/access-control.js index efc9aa9bcb33f..6ad58552bd9b6 100644 --- a/client/my-sites/plugins/access-control.js +++ b/client/my-sites/plugins/access-control.js @@ -1,22 +1,24 @@ /** * External Dependencies */ -var React = require( 'react' ); +import React from 'react' /** * Internal Dependencies */ - var PluginItem = require( 'my-sites/plugins/plugin-item/plugin-item' ), - sites = require( 'lib/sites-list' )(), - i18n = require( 'lib/mixins/i18n' ), - config = require( 'config' ), - analytics = require( 'analytics' ), - isBusiness = require( 'lib/products-values' ).isBusiness, - notices = require( 'notices' ), - abtest = require( 'lib/abtest' ).abtest; +import PluginItem from 'my-sites/plugins/plugin-item/plugin-item' +import sitesList from 'lib/sites-list' +import i18n from 'lib/mixins/i18n' +import config from 'config' +import analytics from 'analytics' +import { isBusiness } from 'lib/products-values' +import notices from 'notices' +import { abtest } from 'lib/abtest' -function hasErrorCondition( site, type ) { - var errorConditions = { +let sites = sitesList(); + +const hasErrorCondition = ( site, type ) => { + const errorConditions = { noBusinessPlan: site && ! site.jetpack && ! isBusiness( site.plan ), notMinimumJetpackVersion: site && ! site.hasMinimumJetpackVersion && site.jetpack, notRightsToManagePlugins: sites.initialized && ! sites.canManageSelectedOrAll() @@ -24,7 +26,7 @@ function hasErrorCondition( site, type ) { return errorConditions[ type ]; } -function getMockBusinessPluginItems() { +const getMockBusinessPluginItems = () => { const plugins = [ { slug: 'ecwid', name: 'Ecwid', @@ -39,7 +41,7 @@ function getMockBusinessPluginItems() { slug: 'shopify', name: 'Shopify', wpcom: true, - icon: '/calypso/images/upgrades/plugins/gumroad.png' + icon: '/calypso/images/upgrades/plugins/shopify-store.png' } ]; const selectedSite = { slug: 'no-slug', @@ -59,8 +61,8 @@ function getMockBusinessPluginItems() { } ); } -function hasRestrictedAccess( site ) { - var pluginPageError; +const hasRestrictedAccess = ( site ) => { + let pluginPageError; site = site || sites.getSelectedSite(); @@ -95,7 +97,7 @@ function hasRestrictedAccess( site ) { action: i18n.translate( 'Upgrade Now' ), actionURL: '/plans/' + site.slug, illustration: '/calypso/images/drake/drake-whoops.svg', - actionCallback: function() { + actionCallback: () => { analytics.tracks.recordEvent( 'calypso_upgrade_nudge_cta_click', { cta_name: 'business_plugins' } ); }, featureExample: getMockBusinessPluginItems() @@ -105,4 +107,4 @@ function hasRestrictedAccess( site ) { return pluginPageError; } -module.exports = { hasRestrictedAccess: hasRestrictedAccess }; +export default { hasRestrictedAccess }; diff --git a/client/my-sites/plugins/plugins-browser-list/index.jsx b/client/my-sites/plugins/plugins-browser-list/index.jsx index 39797d3bcac4a..f2dc6ed9ed169 100644 --- a/client/my-sites/plugins/plugins-browser-list/index.jsx +++ b/client/my-sites/plugins/plugins-browser-list/index.jsx @@ -17,7 +17,7 @@ export default React.createClass( { _DEFAULT_PLACEHOLDER_NUMBER: 6, - getPluginsViewList() { + renderPluginsViewList() { let emptyCounter = 0; let pluginsViewsList = this.props.plugins.map( ( plugin, n ) => { @@ -25,7 +25,7 @@ export default React.createClass( { } ); if ( this.props.showPlaceholders ) { - pluginsViewsList = pluginsViewsList.concat( this.getPlaceholdersViews() ); + pluginsViewsList = pluginsViewsList.concat( this.renderPlaceholdersViews() ); } // We need to complete the list with empty elements to keep the grid drawn. @@ -40,21 +40,21 @@ export default React.createClass( { return pluginsViewsList; }, - getPlaceholdersViews() { + renderPlaceholdersViews() { return Array.apply( null, Array( this.props.size || this._DEFAULT_PLACEHOLDER_NUMBER ) ).map( ( item, i ) => { return ; } ); }, - getViews() { + renderViews() { if ( this.props.plugins.length ) { - return this.getPluginsViewList(); + return this.renderPluginsViewList(); } else if ( this.props.showPlaceholders ) { - return this.getPlaceholdersViews(); + return this.renderPlaceholdersViews(); } }, - getLink() { + renderLink() { if ( this.props.expandedListLink ) { return { this.translate( 'See All' ) } @@ -67,10 +67,10 @@ export default React.createClass( { return (
    - { this.getLink() } + { this.renderLink() } - { this.getViews() } + { this.renderViews() }
    ); From 5d5030a31bdefd2147db41a7320a19f4ef5f1715 Mon Sep 17 00:00:00 2001 From: John Godley Date: Tue, 24 Nov 2015 10:06:43 +0000 Subject: [PATCH 71/89] Globally check for invalid oauth token We need to globally check for a bad oauth token. This indicates that the oauth connection was revoked, and so we should just log out. --- client/lib/wpcom-xhr-wrapper/index.js | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/client/lib/wpcom-xhr-wrapper/index.js b/client/lib/wpcom-xhr-wrapper/index.js index 7e0ab3cac75cb..6f95e3c30f3ee 100644 --- a/client/lib/wpcom-xhr-wrapper/index.js +++ b/client/lib/wpcom-xhr-wrapper/index.js @@ -1,13 +1,29 @@ +/** + * External dependencies + */ import xhr from 'wpcom-xhr-request'; +import debugModule from 'debug'; + +/** + * Module variables + */ +const debug = debugModule( 'calypso:wpcom-xhr-wrapper' ); export default function( params, callback ) { return xhr( params, function( error, response ) { - if ( error && error.response && typeof error.response.body ) { - // Extend the error object in a way to match wpcom-proxy-request - error.httpMessage = error.message; - error.message = error.response.body.message; - error.error = error.response.body.error; - error.statusCode = error.status; + if ( error ) { + if ( error.response && typeof error.response.body ) { + // Extend the error object in a way to match wpcom-proxy-request + error.httpMessage = error.message; + error.message = error.response.body.message; + error.error = error.response.body.error; + error.statusCode = error.status; + } + + if ( error.error === 'invalid_token' ) { + debug( 'Invalid token error detected, authorisation probably revoked - logging out' ); + require( 'lib/user/utils' ).logout(); + } } callback( error, response ); From 2b950e35a6433526e27841f5a6307167c27dc5e6 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Fri, 11 Dec 2015 10:48:56 +0100 Subject: [PATCH 72/89] Gridicons: Add help to the automatic checker error message --- bin/gridiconFormatChecker | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/gridiconFormatChecker b/bin/gridiconFormatChecker index 08fc64dd74608..cb8ab31504f3f 100755 --- a/bin/gridiconFormatChecker +++ b/bin/gridiconFormatChecker @@ -43,7 +43,8 @@ fs.readFile( filename, 'utf8', function ( err, data ) { if ( result !== '' ) { console.error( result ); - console.log( '\033[m=== Valid gridiconsizes are ' + validIconSizes.join( 'px, ' ) + 'px ===\n' ); + console.log( '\033[mValid gridiconsizes are ' + validIconSizes.join( 'px, ' ) + 'px' ); + console.log( '\033[mIf you need any of those Gridicons to have a non-standard size, please, add \'nonStandardSize\' as a property. See https://github.com/Automattic/wp-calypso/tree/master/shared/components/gridicon#props \n' ); process.exit( 1 ); } else { process.exit( 0 ); From 22d8875e5264ab203a934d1b029b87e53f26fd01 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Fri, 11 Dec 2015 11:01:13 +0100 Subject: [PATCH 73/89] Plugin Browser: Add feature example to business error screen --- client/my-sites/plugins/plugins-browser/index.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/my-sites/plugins/plugins-browser/index.jsx b/client/my-sites/plugins/plugins-browser/index.jsx index b60062bf85fc2..6b3b418f4fec8 100644 --- a/client/my-sites/plugins/plugins-browser/index.jsx +++ b/client/my-sites/plugins/plugins-browser/index.jsx @@ -21,6 +21,7 @@ import EmptyContent from 'components/empty-content' import URLSearch from 'lib/mixins/url-search' import infiniteScroll from 'lib/mixins/infinite-scroll' import JetpackManageErrorPage from 'my-sites/jetpack-manage-error-page' +import FeatureExample from 'components/feature-example' module.exports = React.createClass( { @@ -213,7 +214,11 @@ module.exports = React.createClass( { renderAccessError( selectedSite ) { if ( this.state.accessError ) { return ( - + + + + { this.state.accessError.featureExample ? { this.state.accessError.featureExample } : null } + ); } From 48865dc027ecdf225ed3e0f23b7305cdab5526e2 Mon Sep 17 00:00:00 2001 From: Joen Asmussen Date: Fri, 11 Dec 2015 11:46:38 +0100 Subject: [PATCH 74/89] Oauth Login: Hide notice icons The oauth login screen uses the "notices" component in a slightly unusual way to adhere to login screen design standards. Text is centered, and icons are hidden. This PR updates the login screen to accomodate the recent notices change from noticons to gridicons. --- client/auth/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/auth/style.scss b/client/auth/style.scss index 4a64fadb08ea1..899416c615a60 100644 --- a/client/auth/style.scss +++ b/client/auth/style.scss @@ -46,8 +46,8 @@ html.is-desktop { padding: 0; } - .notice.is-info:before { - content: none; + .notice.is-info .notice__icon { + display: none; } } From fe8ab5729db2ec4aceac00535dcf0f39ede25ea4 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Fri, 11 Dec 2015 13:18:02 +0100 Subject: [PATCH 75/89] Components: compact mode for dropdown --- client/components/select-dropdown/README.md | 4 ++ .../select-dropdown/docs/example.jsx | 44 +++++++++++++++++++ client/components/select-dropdown/index.jsx | 1 + client/components/select-dropdown/style.scss | 19 ++++++++ 4 files changed, 68 insertions(+) diff --git a/client/components/select-dropdown/README.md b/client/components/select-dropdown/README.md index 10be3b26c943a..cc998b26ff5a8 100644 --- a/client/components/select-dropdown/README.md +++ b/client/components/select-dropdown/README.md @@ -167,6 +167,10 @@ var options = [ Optional string representing the initial selected option's `value`. Default will be the first option's `value`. +`compact` + +Optional boolean indicating the dropdown will be rendered in compact mode + `onSelect` Optional callback that will be run whenever a new selection has been clicked. diff --git a/client/components/select-dropdown/docs/example.jsx b/client/components/select-dropdown/docs/example.jsx index ed360b2c33b64..e680c275007a6 100644 --- a/client/components/select-dropdown/docs/example.jsx +++ b/client/components/select-dropdown/docs/example.jsx @@ -85,6 +85,50 @@ var SelectDropdownDemo = React.createClass( { Trashed + +

    Compact

    + + + Published + + + + Scheduled + + + + Drafts + + + + + + Trashed + + + +
    +
    ); }, diff --git a/client/components/select-dropdown/index.jsx b/client/components/select-dropdown/index.jsx index 0b564721d464f..81d0bf83b4dfd 100644 --- a/client/components/select-dropdown/index.jsx +++ b/client/components/select-dropdown/index.jsx @@ -156,6 +156,7 @@ var SelectDropdown = React.createClass( { render: function() { const dropdownClasses = { 'select-dropdown': true, + 'is-compact': this.props.compact, 'is-open': this.state.isOpen }; diff --git a/client/components/select-dropdown/style.scss b/client/components/select-dropdown/style.scss index 6bbbb34a24cd1..c0714dfa10b46 100644 --- a/client/components/select-dropdown/style.scss +++ b/client/components/select-dropdown/style.scss @@ -54,11 +54,30 @@ right: 13px; top: 12px; + .is-compact & { + right: 4px; + top: 4px; + } + display: block; line-height: 18px; color: rgba( $gray, 0.5 ); } + + .is-compact & { + padding: 7px; + color: darken( $gray, 10% ); + font-size: 11px; + line-height: 1; + text-transform: uppercase; + + .count { + position: absolute; + top: 5px; + } + } + .select-dropdown.is-open & { border-radius: 4px 4px 0 0; box-shadow: none; From 4b1164b624a0107b71ceecc99772aab5739a6b33 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Fri, 11 Dec 2015 15:35:04 +0100 Subject: [PATCH 76/89] Component: compact button seleccionables in select-dropdown example --- .../select-dropdown/docs/example.jsx | 54 +++++-------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/client/components/select-dropdown/docs/example.jsx b/client/components/select-dropdown/docs/example.jsx index e680c275007a6..7fbb0864f77f0 100644 --- a/client/components/select-dropdown/docs/example.jsx +++ b/client/components/select-dropdown/docs/example.jsx @@ -18,7 +18,8 @@ var SelectDropdownDemo = React.createClass( { getInitialState: function() { return { childSelected: 'Published', - selectedCount: 10 + selectedCount: 10, + compactButtons: false }; }, @@ -34,61 +35,30 @@ var SelectDropdownDemo = React.createClass( { }; }, + toggleButtons: function() { + this.setState( { compactButtons: ! this.state.compactButtons } ); + }, + render: function() { + var toggleButtonsText = this.state.compactButtons ? 'Normal Buttons' : 'Compact Buttons'; + return (

    Select Dropdown

    +
    { toggleButtonsText } +

    items passed as options prop

    items passed as children

    - - Published - - - - Scheduled - - - - Drafts - - - - - - Trashed - - - -

    Compact

    - Date: Tue, 8 Dec 2015 12:22:41 +0100 Subject: [PATCH 77/89] Notices: notices/site-notice -> components/notice Notices: notices/site-notice->components/notice in current-site Notices: fix bottom margin in notices displayed in sidebar So it matches previous design of site-notice class. Notices: Remove unused css of site-notice The component is no longer uses and will be removed shortly Notices: Remove unused site-notice component --- client/components/notice/style.scss | 5 +++++ client/my-sites/current-site/index.jsx | 18 +++++++++++------ client/notices/site-notice.jsx | 27 -------------------------- client/notices/style.scss | 27 -------------------------- 4 files changed, 17 insertions(+), 60 deletions(-) delete mode 100644 client/notices/site-notice.jsx diff --git a/client/components/notice/style.scss b/client/components/notice/style.scss index 5ff6b4748fa41..5785bba192930 100644 --- a/client/components/notice/style.scss +++ b/client/components/notice/style.scss @@ -200,3 +200,8 @@ background: rgba( 255, 255, 255, 0.2 ); } } + +// For site notices +.current-site.card .notice.is-compact { + margin-bottom: 0px +} diff --git a/client/my-sites/current-site/index.jsx b/client/my-sites/current-site/index.jsx index 61967990583ce..e3fd727c983c4 100644 --- a/client/my-sites/current-site/index.jsx +++ b/client/my-sites/current-site/index.jsx @@ -12,7 +12,8 @@ var React = require( 'react/addons' ), var AllSites = require( 'my-sites/all-sites' ), AddNewButton = require( 'components/add-new-button' ), Card = require( 'components/card' ), - SiteNotice = require( 'notices/site-notice' ), + Notice = require( 'components/notice' ), + NoticeAction = require( 'components/notice/notice-action' ), layoutFocus = require( 'lib/layout-focus' ), Site = require( 'my-sites/site' ), Gridicon = require( 'components/gridicon' ), @@ -98,12 +99,17 @@ module.exports = React.createClass( { } return ( - - { this.translate( 'The site redirects to {{a}}%(url)s{{/a}}', { + } - } ) } - + components: { a: } + } ) }> + + { this.translate( 'Edit' ) } + + ); }, diff --git a/client/notices/site-notice.jsx b/client/notices/site-notice.jsx deleted file mode 100644 index 673058fa5f36d..0000000000000 --- a/client/notices/site-notice.jsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * External Dependencies - */ - -var React = require( 'react' ), - classNames = require( 'classnames' ); - -/** - * Internal Dependencies - */ - -module.exports = React.createClass( { - props: { - status: React.PropTypes.string - }, - displayName: 'SiteNotice', - - render: function() { - let status = this.props.status || 'is-info', - classes = classNames( 'site-notice', status ); - return ( - - { this.props.children } - - ); - } -} ); diff --git a/client/notices/style.scss b/client/notices/style.scss index 168c82b809434..f7d09c355470b 100644 --- a/client/notices/style.scss +++ b/client/notices/style.scss @@ -27,30 +27,3 @@ width: 100%; display: block; } - -.site-notice { - display: block; - line-height: 1.8; - overflow: hidden; - padding: 8px 18px 8px 18px; - text-overflow: ellipsis; - color: $white; - - a { - color: rgba( $white, 0.85 ); - text-decoration: underline; - font-weight: 600; - } - - &.is-info { - background-color: $blue-wordpress; - } - - &.is-warning { - background-color: $alert-yellow; - } - - &.is-error { - background-color: $alert-red; - } -} From d0d67bb9067518b4dab24daffa3d8faeebe695d7 Mon Sep 17 00:00:00 2001 From: johnHackworth Date: Fri, 11 Dec 2015 16:59:26 +0100 Subject: [PATCH 78/89] Components: Removed count borders when inside of a compact dropdown --- client/components/select-dropdown/style.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/components/select-dropdown/style.scss b/client/components/select-dropdown/style.scss index c0714dfa10b46..2e36715e16078 100644 --- a/client/components/select-dropdown/style.scss +++ b/client/components/select-dropdown/style.scss @@ -73,8 +73,9 @@ text-transform: uppercase; .count { - position: absolute; - top: 5px; + border-width: 0; + margin-left: 0; + line-height: 1; } } From 30c5b1ae34ecfeb6df2349151c269c16e7deb0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Ventura?= Date: Fri, 11 Dec 2015 17:22:41 +0100 Subject: [PATCH 79/89] Remove opacity changes in . --- client/components/select-dropdown/style.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/components/select-dropdown/style.scss b/client/components/select-dropdown/style.scss index 2e36715e16078..6fbd1215245a3 100644 --- a/client/components/select-dropdown/style.scss +++ b/client/components/select-dropdown/style.scss @@ -91,7 +91,6 @@ .count { margin-left: 8px; - opacity: 0.5; } } @@ -191,7 +190,6 @@ justify-content: space-between; .count { - opacity: 0.5; color: inherit; border-color: inherit; } From c95536c8eb5e4b35feeb4153cc2e7d6f40540ba9 Mon Sep 17 00:00:00 2001 From: Mel Choyce Date: Fri, 11 Dec 2015 12:08:23 -0500 Subject: [PATCH 80/89] Update Stats empty-state modules to use the correct background color and update the RGBA values to use color variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The style was using the old background color, which meant the module wasn’t actually reaching transparency at the bottom. --- client/my-sites/stats/nux/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/my-sites/stats/nux/style.scss b/client/my-sites/stats/nux/style.scss index b97cc2aa05a4f..3504cca284814 100644 --- a/client/my-sites/stats/nux/style.scss +++ b/client/my-sites/stats/nux/style.scss @@ -7,7 +7,7 @@ // to hide module's default border width: 101%; left: -1px; - background: linear-gradient(to bottom, rgba(233,239,243,0), rgba(233,239,243,0.3), rgba(233,239,243,1) 95%); + background: linear-gradient(to bottom, rgba( lighten( $gray, 30% ), 0 ), rgba( lighten( $gray, 30% ), 0.3 ), $gray-light 95%); @include breakpoint( "<660px" ) { // hide module's bottom border that's showing on mobile From 807d1647fc526fc84789b49aede2d5f4ade49a15 Mon Sep 17 00:00:00 2001 From: Mel Choyce Date: Fri, 11 Dec 2015 12:17:15 -0500 Subject: [PATCH 81/89] Updating all colors to use $gray-light --- client/my-sites/stats/nux/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/my-sites/stats/nux/style.scss b/client/my-sites/stats/nux/style.scss index 3504cca284814..04e1c45d13710 100644 --- a/client/my-sites/stats/nux/style.scss +++ b/client/my-sites/stats/nux/style.scss @@ -7,7 +7,7 @@ // to hide module's default border width: 101%; left: -1px; - background: linear-gradient(to bottom, rgba( lighten( $gray, 30% ), 0 ), rgba( lighten( $gray, 30% ), 0.3 ), $gray-light 95%); + background: linear-gradient(to bottom, rgba( $gray-light, 0 ), rgba( $gray-light, 0.3 ), $gray-light 95%); @include breakpoint( "<660px" ) { // hide module's bottom border that's showing on mobile From 669917562fbea669d8329b7d95349fb2b79b7aad Mon Sep 17 00:00:00 2001 From: Rick Banister Date: Thu, 10 Dec 2015 10:46:56 -0500 Subject: [PATCH 82/89] Framework: removed last remaining instance of SectionHeaderButton and forever banished that component to hell --- client/components/section-header/README.md | 26 +++++++------- client/components/section-header/button.jsx | 34 ------------------- .../section-header/docs/example.jsx | 21 +++++++----- .../card/header/primary-domain-button.jsx | 7 ++-- 4 files changed, 29 insertions(+), 59 deletions(-) delete mode 100644 client/components/section-header/button.jsx diff --git a/client/components/section-header/README.md b/client/components/section-header/README.md index 4f4075fb4af2e..ba0d2bc875651 100644 --- a/client/components/section-header/README.md +++ b/client/components/section-header/README.md @@ -8,31 +8,29 @@ and optional actions buttons. ```js var SectionHeader = require( 'components/section-header' ), - SectionHeaderButton = require( 'components/section-header/button' ); + Button = require( 'components/button' ); render: function() { return ( - Manage - + + ); } ``` ## Section Header -This is the base component and acts as a wrapper for -the People Section Header Buttons. +This is the base component and acts as a wrapper for a section's (a list of cards) title and any action buttons that act upon that list (like Bulk Edit or Add New Item). #### Props - `className` - *optional* (string|object) Classes to be added to the rendered component. - `label` - *optional* (string) The text to be displayed in the header. - -## People Section Header Button -This component acts as a wrapper around the `button` component and -forces the `section-header__button` class be rendered. This component, -when passed as a child to `SectionHeader`, is rendered as -an action button on the right hand side of the header. diff --git a/client/components/section-header/button.jsx b/client/components/section-header/button.jsx deleted file mode 100644 index 53c4d1aae3fc5..0000000000000 --- a/client/components/section-header/button.jsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * External dependencies - */ -var React = require( 'react' ), - classNames = require( 'classnames' ), - omit = require( 'lodash/object/omit' ); - -var peopleSectionHeaderButton = React.createClass( { - - getDefaultProps: function() { - return { - className: '' - }; - }, - - render: function() { - var buttonClasses = classNames( - 'button', - 'section-header__button', - this.props.className - ); - - return ( - - ); - } -} ); - -module.exports = peopleSectionHeaderButton; diff --git a/client/components/section-header/docs/example.jsx b/client/components/section-header/docs/example.jsx index 6860b5ab93f08..16af10e0cded9 100644 --- a/client/components/section-header/docs/example.jsx +++ b/client/components/section-header/docs/example.jsx @@ -7,7 +7,7 @@ var React = require( 'react' ); * Internal dependencies */ var SectionHeader = require( 'components/section-header' ), - SectionHeaderButton = require( 'components/section-header/button' ), + Button = require( 'components/button' ), Card = require( 'components/card' ); var Cards = React.createClass( { @@ -23,16 +23,21 @@ var Cards = React.createClass( { - Manage - - Add - + + - Content here + { this.translate( 'Content Here' ) }
    ); diff --git a/client/my-sites/upgrades/domain-management/edit/card/header/primary-domain-button.jsx b/client/my-sites/upgrades/domain-management/edit/card/header/primary-domain-button.jsx index 07ddcdd343af4..0074c842d05a5 100644 --- a/client/my-sites/upgrades/domain-management/edit/card/header/primary-domain-button.jsx +++ b/client/my-sites/upgrades/domain-management/edit/card/header/primary-domain-button.jsx @@ -9,7 +9,7 @@ const React = require( 'react' ), */ const analyticsMixin = require( 'lib/mixins/analytics' ), paths = require( 'my-sites/upgrades/paths' ), - SectionHeaderButton = require( 'components/section-header/button' ); + Button = require( 'components/button' ); const PrimaryDomainButton = React.createClass( { mixins: [ analyticsMixin( 'domainManagement', 'edit' ) ], @@ -41,11 +41,12 @@ const PrimaryDomainButton = React.createClass( { } return ( - { label } - + ); } From e0247749dd22b90b478167262a3f9b0342376b33 Mon Sep 17 00:00:00 2001 From: Rick Banister Date: Thu, 10 Dec 2015 11:16:40 -0500 Subject: [PATCH 83/89] SectionHeader: removed reference to SectionHeaderButton in the css, fixed the css to properly target Button --- client/components/section-header/style.scss | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/client/components/section-header/style.scss b/client/components/section-header/style.scss index d149a5b5c3edf..dda41b25d4699 100644 --- a/client/components/section-header/style.scss +++ b/client/components/section-header/style.scss @@ -33,17 +33,15 @@ @include clear-fix; } -.section-header__label, -.section-header__button { +.section-header__label { color: $gray; font-size: 11px; text-transform: uppercase; } -.section-header__button { - background: none; +.section-header .button { + float: left; margin-right: 8px; - padding: 2px 8px; &:last-child { margin-right: 0; From 6d62f40bb47dc4d58bd40b7737066d191c12d307 Mon Sep 17 00:00:00 2001 From: Rick Banister Date: Thu, 10 Dec 2015 11:18:18 -0500 Subject: [PATCH 84/89] SectionHeader: added a Primary Button to the devdoc example --- client/components/section-header/docs/example.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/components/section-header/docs/example.jsx b/client/components/section-header/docs/example.jsx index 16af10e0cded9..e09660c4df893 100644 --- a/client/components/section-header/docs/example.jsx +++ b/client/components/section-header/docs/example.jsx @@ -23,6 +23,11 @@ var Cards = React.createClass( { + ); diff --git a/client/components/section-header/docs/example.jsx b/client/components/section-header/docs/example.jsx index e09660c4df893..f2b8ffc81c1bb 100644 --- a/client/components/section-header/docs/example.jsx +++ b/client/components/section-header/docs/example.jsx @@ -23,26 +23,24 @@ var Cards = React.createClass( { - - - { this.translate( 'Content Here' ) } + Content Here
    ); From f549e388180a43a01aab7f1885797d83c7b1382e Mon Sep 17 00:00:00 2001 From: Rodrigo Iloro Date: Mon, 23 Nov 2015 13:40:13 -0300 Subject: [PATCH 86/89] Editor: Contact Form Builder Implementation of a basic TinyMCE plugin and dialog window to add the default contact form to a post. Supports switching views and multiple forms. --- client/components/tinymce/index.jsx | 14 ++- .../tinymce/plugins/contact-form/dialog.jsx | 102 ++++++++++++++++++ .../tinymce/plugins/contact-form/plugin.js | 67 ++++++++++++ .../tinymce/plugins/contact-form/style.scss | 0 .../plugins/wpcom-view/contact-form-view.jsx | 42 ++++++++ .../tinymce/plugins/wpcom-view/views.js | 8 +- client/components/tinymce/style.scss | 5 + config/development.json | 1 + public/tinymce/skins/wordpress/wp-content.css | 7 ++ 9 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 client/components/tinymce/plugins/contact-form/dialog.jsx create mode 100644 client/components/tinymce/plugins/contact-form/plugin.js create mode 100644 client/components/tinymce/plugins/contact-form/style.scss create mode 100644 client/components/tinymce/plugins/wpcom-view/contact-form-view.jsx diff --git a/client/components/tinymce/index.jsx b/client/components/tinymce/index.jsx index 876498a163a63..309e962b89395 100644 --- a/client/components/tinymce/index.jsx +++ b/client/components/tinymce/index.jsx @@ -36,6 +36,7 @@ require( './plugins/wpcom-tabindex/plugin' )(); require( './plugins/touch-scroll-toolbar/plugin' )(); require( './plugins/editor-button-analytics/plugin' )(); require( './plugins/calypso-alert/plugin' )(); +require( './plugins/contact-form/plugin' )(); /** * Internal Dependencies @@ -43,7 +44,8 @@ require( './plugins/calypso-alert/plugin' )(); const formatting = require( 'lib/formatting' ), user = require( 'lib/user' )(), i18n = require( './i18n' ), - viewport = require( 'lib/viewport' ); + viewport = require( 'lib/viewport' ), + config = require( 'config' ); /** * Internal Variables @@ -94,6 +96,7 @@ const PLUGINS = [ 'wpcom/editorbuttonanalytics', 'wpcom/calypsoalert', 'wpcom/tabindex', + 'wpcom/contactform' ]; const CONTENT_CSS = [ @@ -177,6 +180,11 @@ module.exports = React.createClass( { this.localize(); + let toolbar1 = [ 'wpcom_add_media', 'formatselect', 'bold', 'italic', 'bullist', 'numlist', 'link', 'blockquote', 'alignleft', 'aligncenter', 'alignright', 'spellchecker', 'wp_more', 'wpcom_advanced' ]; + if ( config.isEnabled( 'post-editor/contact-form' ) ) { + toolbar1.splice( 1, 0, 'wpcom_add_contact_form' ); + } + tinymce.init( { selector: '#' + this._id, skin_url: '//s1.wp.com/wp-includes/js/tinymce/skins/lightgray', @@ -231,14 +239,14 @@ module.exports = React.createClass( { // Limit the preview styles in the menu/toolbar preview_styles: 'font-family font-size font-weight font-style text-decoration text-transform', end_container_on_empty_block: true, - plugins: PLUGINS.join( ',' ), + plugins: PLUGINS.join(), statusbar: false, resize: false, menubar: false, indent: false, autoresize_min_height: document.documentElement.clientHeight, - toolbar1: 'wpcom_add_media,formatselect,bold,italic,bullist,numlist,link,blockquote,alignleft,aligncenter,alignright,spellchecker,wp_more,wpcom_advanced', + toolbar1: toolbar1.join(), toolbar2: 'strikethrough,underline,hr,alignjustify,forecolor,pastetext,removeformat,wp_charmap,outdent,indent,undo,redo,wp_help', toolbar3: '', toolbar4: '', diff --git a/client/components/tinymce/plugins/contact-form/dialog.jsx b/client/components/tinymce/plugins/contact-form/dialog.jsx new file mode 100644 index 0000000000000..968a2b5240816 --- /dev/null +++ b/client/components/tinymce/plugins/contact-form/dialog.jsx @@ -0,0 +1,102 @@ +/** + * External dependencies + */ +import React, { PropTypes } from 'react'; + +/** + * Internal dependencies + */ +import Shortcode from 'lib/shortcode'; +import Dialog from 'components/dialog'; +import FormFieldset from 'components/forms/form-fieldset'; +import FormLabel from 'components/forms/form-label'; +import FormButton from 'components/forms/form-button'; + +/** + * object constants + */ +const fieldTypes = { + name: 'name', + email: 'email', + url: 'url', + checkbox: 'checkbox', + dropdown: 'dropdown', + radio: 'radio', + text: 'text', + textarea: 'textarea', + website: 'website' +}; + +const defaultForm = [ + { label: 'Name', type: fieldTypes.name, required: true }, + { label: 'Email', type: fieldTypes.email, required: true }, + { label: 'Website', type: fieldTypes.url }, + { label: 'Comment', type: fieldTypes.textarea, required: true } +]; + +export default React.createClass( { + displayName: 'ContactFormDialog', + + propTypes: { + onClose: PropTypes.func.isRequired, + onInsertMedia: PropTypes.func.isRequired, + showDialog: PropTypes.bool.isRequired + }, + + render() { + const buttons = [ + { + const fields = defaultForm.map( field => { + return Shortcode.stringify( { + tag: 'contact-field', + type: 'self-closing', + attrs: { + label: field.label, + type: field.type, + required: field.required ? 1 : 0 + } + } ); + } ).join( '' ); + + const shortcode = Shortcode.stringify( { + tag: 'contact-form', + type: 'closed', + content: fields, + attrs: { + to: 'user@example.com', + subject: 'this is a contact form' + } + } ); + + this.props.onInsertMedia( shortcode ); + } } + > + { this.translate( 'Save' ) } + , + + { this.translate( 'Cancel' ) } + + ]; + + return ( + + + + Here be dragons. Click Save to add a generic contact form... + + + + ); + } +} ); diff --git a/client/components/tinymce/plugins/contact-form/plugin.js b/client/components/tinymce/plugins/contact-form/plugin.js new file mode 100644 index 0000000000000..e1100c815dced --- /dev/null +++ b/client/components/tinymce/plugins/contact-form/plugin.js @@ -0,0 +1,67 @@ +/** + * External Dependencies + */ +import tinymce from 'tinymce/tinymce'; +import i18n from 'lib/mixins/i18n'; +import React from 'react'; + +/** + * Internal Dependencies + */ +import Gridicon from 'components/gridicon'; +import ContactFormDialog from './dialog'; + +const contactForm = editor => { + let node; + + editor.on( 'init', () => { + node = editor.getContainer().appendChild( + document.createElement( 'div' ) + ); + } ); + + editor.on( 'remove', () => { + React.unmountComponentAtNode( node ); + node.parentNode.removeChild( node ); + node = null; + } ); + + editor.addCommand( 'WP_ContactForm', () => { + function onClose() { + editor.focus(); + renderModal( 'hide' ); + }; + + function renderModal( visibility = 'show' ) { + React.render( + React.createElement( ContactFormDialog, { + showDialog: visibility === 'show', + onClose, + onInsertMedia( markup ) { + editor.execCommand( 'mceInsertContent', false, markup ); + } + } ), + node + ); + }; + + renderModal(); + } ); + + editor.addButton( 'wpcom_add_contact_form', { + classes: 'btn wpcom-button contact-form', + title: i18n.translate( 'Add Contact Form' ), + cmd: 'WP_ContactForm', + onPostRender() { + this.innerHtml( React.renderToStaticMarkup( + + ) ); + } + } ); +}; + +export default () => { + tinymce.PluginManager.add( 'wpcom/contactform', contactForm ); +} diff --git a/client/components/tinymce/plugins/contact-form/style.scss b/client/components/tinymce/plugins/contact-form/style.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/client/components/tinymce/plugins/wpcom-view/contact-form-view.jsx b/client/components/tinymce/plugins/wpcom-view/contact-form-view.jsx new file mode 100644 index 0000000000000..a7ea2ea593d43 --- /dev/null +++ b/client/components/tinymce/plugins/wpcom-view/contact-form-view.jsx @@ -0,0 +1,42 @@ +/** + * External dependencies + */ +import React from 'react'; + +/** + * Internal dependecies + */ +import shortcodeUtils from 'lib/shortcode'; + +export default React.createClass( { + statics: { + match( content ) { + const match = shortcodeUtils.next( 'contact-form', content ); + + if ( match ) { + return { + index: match.index, + content: match.content, + options: { + shortcode: match.shortcode + } + }; + } + }, + + serialize( content ) { + return encodeURIComponent( content ); + }, + + edit( editor, content ) { + editor.execCommand( 'WP_ContactForm', content ); + } + }, + render() { + return ( +
    +

    This is a placeholder for the form preview.

    +
    + ); + } +} ); diff --git a/client/components/tinymce/plugins/wpcom-view/views.js b/client/components/tinymce/plugins/wpcom-view/views.js index 7989f3cdb89d9..c567cc697f7b7 100644 --- a/client/components/tinymce/plugins/wpcom-view/views.js +++ b/client/components/tinymce/plugins/wpcom-view/views.js @@ -10,17 +10,23 @@ import values from 'lodash/object/values'; /** * Internal dependencies */ +import config from 'config'; import GalleryView from './gallery-view'; import EmbedViewManager from './views/embed'; +import ContactFormView from './contact-form-view'; /** * Module variables */ -const views = { +let views = { gallery: GalleryView, embed: new EmbedViewManager() }; +if ( config.isEnabled( 'post-editor/contact-form' ) ) { + views.contact = ContactFormView; +} + const components = mapValues( views, ( view ) => { if ( 'function' === typeof view.getComponent ) { return view.getComponent(); diff --git a/client/components/tinymce/style.scss b/client/components/tinymce/style.scss index 0b75280a538da..29d32cecdfd8f 100644 --- a/client/components/tinymce/style.scss +++ b/client/components/tinymce/style.scss @@ -158,6 +158,7 @@ vertical-align: middle; } +.mce-toolbar .mce-wpcom-button.mce-contact-form button, .mce-toolbar .mce-wpcom-button.mce-media button { height: 38px; padding: 0 8px; @@ -178,21 +179,25 @@ font-size: 13px; } +.mce-toolbar .mce-wpcom-button.mce-contact-form:hover span, .mce-toolbar .mce-wpcom-button.mce-media:hover span { color: $gray-dark; } +.mce-toolbar .mce-wpcom-button.mce-contact-form svg, .mce-toolbar .mce-wpcom-button.mce-media svg { fill: darken( $gray, 20% ); width: 20px; margin-right: 4px; } +.mce-toolbar .mce-wpcom-button.mce-contact-form:hover svg, .mce-toolbar .mce-wpcom-button.mce-media:hover svg { fill: $gray-dark; } // Position media button at the beginning of the toolbar +.mce-toolbar .mce-wpcom-button.mce-btn.mce-contact-form, .mce-toolbar .mce-wpcom-button.mce-btn.mce-media { margin: 0; border: 0; diff --git a/config/development.json b/config/development.json index 4c1d93f21bd89..ac5f2a50bde07 100644 --- a/config/development.json +++ b/config/development.json @@ -43,6 +43,7 @@ "post-editor/pages": true, "post-editor-github-link": false, "post-editor/post-type-switch": false, + "post-editor/contact-form": true, "manage/media": true, "manage/posts": true, diff --git a/public/tinymce/skins/wordpress/wp-content.css b/public/tinymce/skins/wordpress/wp-content.css index dd7b5c48db491..58deb2f419ac0 100644 --- a/public/tinymce/skins/wordpress/wp-content.css +++ b/public/tinymce/skins/wordpress/wp-content.css @@ -416,6 +416,13 @@ audio { clear: both; } +.wpview-type-contact-form { + border: 1px solid #87a6bc; + border-radius: 3px; + padding: 20px; + text-align: center; +} + .gallery img[data-mce-selected]:focus { outline: none; } From e669d928563cb908a72ee370aeb26c553b5d0a31 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 10 Dec 2015 15:13:41 -0500 Subject: [PATCH 87/89] Framework: Add mixedindentlint target to Makefile Scans the JS and SASS files. Also added as a dependency to the `lint` target. --- Makefile | 6 +++++- package.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5794d0cc15ccf..9df4ce181cd35 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,7 @@ node_modules: package.json test: build @$(BIN)/run-all-tests -lint: node_modules/eslint node_modules/eslint-plugin-react node_modules/babel-eslint +lint: node_modules/eslint node_modules/eslint-plugin-react node_modules/babel-eslint mixedindentlint @$(NODE_BIN)/eslint --quiet $(JS_FILES) eslint: lint @@ -85,6 +85,10 @@ eslint-branch: node_modules/eslint node_modules/eslint-plugin-react node_modules i18n-lint: @echo "$(JS_FILES)" | sed 's/\([^ ]*\/test\/[^ ]* *\)//g' | xargs -n1 $(I18NLINT) +# Skip files that are auto-generated +mixedindentlint: node_modules/mixedindentlint + @echo "$(JS_FILES)\n$(SASS_FILES)" | xargs $(NODE_BIN)/mixedindentlint --ignore-comments --exclude="client/config/index.js" --exclude="shared/components/gridicon/index.jsx" + # keep track of the current CALYPSO_ENV so that it can be used as a # prerequisite for other rules .env: FORCE diff --git a/package.json b/package.json index 10394c6d72838..ef8bbf38dd365 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "jsdom": "3.1.2", "localStorage": "1.0.2", "lodash-deep": "1.5.3", + "mixedindentlint": "1.1.1", "mocha": "2.3.4", "mockery": "1.4.0", "nock": "2.17.0", From e1f72a6d2f7e8ac23aceade8db725bed63b2fc55 Mon Sep 17 00:00:00 2001 From: Lance Willett Date: Fri, 11 Dec 2015 12:16:46 -0700 Subject: [PATCH 88/89] Fix Hello World link, see #831 --- docs/guide/hello-world.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/hello-world.md b/docs/guide/hello-world.md index 91c3f8d0264a1..09568eaf4e875 100644 --- a/docs/guide/hello-world.md +++ b/docs/guide/hello-world.md @@ -114,7 +114,7 @@ Restart the server doing: * `make run` -We are ready to load http://calypso.localhost:3000/hello-world! Your console should respond with `Hello, world?` if everything is working and you should see Calypso's sidebar for "My Sites". +We are ready to load [http://calypso.localhost:3000/hello-world](http://calypso.localhost:3000/hello-world)! Your console should respond with `Hello, world?` if everything is working and you should see Calypso's sidebar for "My Sites". ---- From e362b8d76aff5fe49b45c15b21d99fd661411e50 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Wed, 9 Dec 2015 16:09:19 +0000 Subject: [PATCH 89/89] Signup: Free trials: Only show free trials on signup in the dev environment --- .../components/plans/plan-actions/index.jsx | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/client/components/plans/plan-actions/index.jsx b/client/components/plans/plan-actions/index.jsx index c6706e60389ae..be4c3c9124348 100644 --- a/client/components/plans/plan-actions/index.jsx +++ b/client/components/plans/plan-actions/index.jsx @@ -27,7 +27,7 @@ module.exports = React.createClass( { } if ( this.props.isInSignup ) { - return this.signUpActions(); + return config.isEnabled( 'upgrades/free-trials' ) ? this.newPlanActions() : this.upgradeActions(); } if ( this.siteHasThisPlan() ) { @@ -147,26 +147,6 @@ module.exports = React.createClass( { ); }, - signUpActions: function() { - if ( isFreePlan( this.props.plan ) ) { - return
    - -
    ; - } - - return ( -
    - -
    - ); - }, - newPlanActions: function() { if ( isFreePlan( this.props.plan ) ) { return