diff --git a/bin/gridiconFormatChecker b/bin/gridiconFormatChecker new file mode 100755 index 0000000000000..08fc64dd74608 --- /dev/null +++ b/bin/gridiconFormatChecker @@ -0,0 +1,51 @@ +#!/usr/bin/env node + +var fs = require( 'fs' ); +var validIconSizes = [ 12, 18, 24, 36, 48, 54, 72 ]; + +var filename = process.argv[ 2 ]; + +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( ' 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( 'nonstandardsize' ) >= 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'; + } + 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'; + } + } + } + lineNumber += chunk.split('\n').length - 1; + } + } ); + } + + if ( result !== '' ) { + console.error( result ); + console.log( '\033[m=== Valid gridiconsizes are ' + validIconSizes.join( 'px, ' ) + 'px ===\n' ); + process.exit( 1 ); + } else { + process.exit( 0 ); + } +} ); 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 diff --git a/client/boot/index.js b/client/boot/index.js index 22a432d256eda..398a1b5832b21 100644 --- a/client/boot/index.js +++ b/client/boot/index.js @@ -11,6 +11,7 @@ var React = require( 'react' ), page = require( 'page' ), url = require( 'url' ), qs = require( 'querystring' ), + ReduxProvider = require( 'react-redux' ).Provider, injectTapEventPlugin = require( 'react-tap-event-plugin' ); /** @@ -78,9 +79,7 @@ function init() { } ); } -function setUpContext( layout ) { - var reduxStore = createReduxStore(); - +function setUpContext( layout, reduxStore ) { // Pass the layout so that it is available to all page handlers // and add query and hash objects onto context object page( '*', function( context, next ) { @@ -138,7 +137,7 @@ function loadDevModulesAndBoot() { } function boot() { - var layoutSection, layout, validSections = []; + var layoutSection, layout, layoutComponentCreator, reduxStore, validSections = []; init(); @@ -153,6 +152,7 @@ function boot() { } ); translatorJumpstart.init(); + reduxStore = createReduxStore(); if ( user.get() ) { // When logged in the analytics module requires user and superProps objects @@ -161,13 +161,14 @@ function boot() { // Create layout instance with current user prop Layout = require( 'layout' ); - layout = React.render( React.createElement( Layout, { + + layoutComponentCreator = () => React.createElement( Layout, { user: user, sites: sites, focus: layoutFocus, nuxWelcome: nuxWelcome, translatorInvitation: translatorInvitation - } ), document.getElementById( 'wpcom' ) ); + } ); } else { analytics.setSuperProps( superProps ); @@ -177,12 +178,14 @@ function boot() { LoggedOutLayout = require( 'layout/logged-out' ); } - layout = React.render( - React.createElement( LoggedOutLayout ), - document.getElementById( 'wpcom' ) - ); + layoutComponentCreator = () => React.createElement( LoggedOutLayout ); } + layout = React.render( + React.createElement( ReduxProvider, { store: reduxStore }, layoutComponentCreator ), + document.getElementById( 'wpcom' ) + ); + debug( 'Main layout rendered.' ); // If `?sb` or `?sp` are present on the path set the focus of layout @@ -193,7 +196,7 @@ function boot() { window.history.replaceState( null, document.title, window.location.pathname ); } - setUpContext( layout ); + setUpContext( layout, reduxStore ); page( '*', require( 'lib/route/normalize' ) ); diff --git a/client/components/overlay/overlay.jsx b/client/components/overlay/overlay.jsx index d428302d6d2c6..4ca732eadb57a 100644 --- a/client/components/overlay/overlay.jsx +++ b/client/components/overlay/overlay.jsx @@ -9,6 +9,7 @@ var React = require( 'react/addons' ), * Internal dependencies */ var Toolbar = require( './toolbar' ), + GlobalNotices = require( 'notices/global-notices' ), NoticesList = require( 'notices/notices-list' ), notices = require( 'notices' ), page = require( 'page' ), @@ -101,6 +102,7 @@ module.exports = React.createClass({
+ { this.props.children }
diff --git a/client/components/section-header/style.scss b/client/components/section-header/style.scss index 58738369b6725..d149a5b5c3edf 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 ); } @@ -33,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; diff --git a/client/layout/index.jsx b/client/layout/index.jsx index 81b696f292f25..20c0dc2ffb03e 100644 --- a/client/layout/index.jsx +++ b/client/layout/index.jsx @@ -11,6 +11,7 @@ var React = require( 'react' ), */ var Masterbar = require( './masterbar' ), observe = require( 'lib/mixins/data-observe' ), + GlobalNotices = require( 'notices/global-notices' ), NoticesList = require( 'notices/notices-list' ), notices = require( 'notices' ), translator = require( 'lib/translator-jumpstart' ), @@ -111,6 +112,7 @@ module.exports = React.createClass( { +
diff --git a/client/layout/logged-out.jsx b/client/layout/logged-out.jsx index 3f4ba8df5cea6..83f6ad3c4f1ca 100644 --- a/client/layout/logged-out.jsx +++ b/client/layout/logged-out.jsx @@ -9,6 +9,7 @@ var React = require( 'react' ), */ var Masterbar = require( './masterbar' ), NoticesList = require( 'notices/notices-list' ), + GlobalNotices = require( 'notices/global-notices' ), notices = require( 'notices' ); module.exports = React.createClass( { @@ -32,6 +33,7 @@ module.exports = React.createClass( {
+
diff --git a/client/me/controller.js b/client/me/controller.js index daa2e4a5d28e2..c0960713f5cc6 100644 --- a/client/me/controller.js +++ b/client/me/controller.js @@ -18,6 +18,7 @@ import purchasesController from './purchases/controller'; import userFactory from 'lib/user'; import userSettings from 'lib/user-settings'; import titleActions from 'lib/screen-title/actions'; +import { Provider } from 'react-redux'; const ANALYTICS_PAGE_TITLE = 'Me', devices = devicesFactory(), @@ -192,15 +193,17 @@ export default { analytics.pageView.record( basePath, ANALYTICS_PAGE_TITLE + ' > Notifications' ); React.render( - React.createElement( NotificationsComponent, - { - user: user, - userSettings: userSettings, - blogs: sites, - devices: devices, - path: context.path - } - ), + React.createElement( Provider, { store: context.store }, () => { + return React.createElement( NotificationsComponent, + { + user: user, + userSettings: userSettings, + blogs: sites, + devices: devices, + path: context.path + } + ) + } ), document.getElementById( 'primary' ) ); }, diff --git a/client/me/notification-settings/index.jsx b/client/me/notification-settings/index.jsx index 7068ddaa337b3..2cd7995586d40 100644 --- a/client/me/notification-settings/index.jsx +++ b/client/me/notification-settings/index.jsx @@ -7,7 +7,7 @@ import React from 'react'; * Internal dependencies */ import observe from 'lib/mixins/data-observe'; -import notices from 'notices'; +import noticeActionCreators from 'state/notices/actionCreators' import Main from 'components/main'; import ReauthRequired from 'me/reauth-required'; import twoStepAuthorization from 'lib/two-step-authorization'; @@ -16,8 +16,9 @@ import Navigation from './navigation'; import BlogsSettings from './blogs-settings'; import store from 'lib/notification-settings-store'; import { fetchSettings, toggle, saveSettings } from 'lib/notification-settings-store/actions'; +import { connect } from 'react-redux'; -export default React.createClass( { +const NotificationSettings = React.createClass( { displayName: 'NotificationSettings', mixins: [ observe( 'sites', 'devices' ) ], @@ -43,11 +44,11 @@ export default React.createClass( { const state = store.getStateFor( 'blogs' ); if ( state.error ) { - notices.error( this.translate( 'There was a problem saving your changes. Please, try again.' ) ); + this.props.errorNotice( this.translate( 'There was a problem saving your changes. Please, try again.' ) ); } if ( state.status === 'success' ) { - notices.success( this.translate( 'Settings saved successfully!' ) ); + this.props.successNotice( this.translate( 'Settings saved successfully!' ), { duration: 3000 } ); } this.setState( state ); @@ -74,3 +75,9 @@ export default React.createClass( { } } ); +export default connect( + () => { + return {} + }, + noticeActionCreators +)( NotificationSettings ); 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 ( { return ; - }, this ); + } ); if ( this.props.showPlaceholders ) { pluginsViewsList = pluginsViewsList.concat( this.getPlaceholdersViews() ); @@ -39,13 +40,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 +54,7 @@ module.exports = React.createClass( { } }, - getLink: function() { + getLink() { if ( this.props.expandedListLink ) { return { this.translate( 'See All' ) } @@ -62,18 +63,15 @@ module.exports = React.createClass( { } }, - render: function() { + render() { return (
-
-

- { this.props.title } -

+ { 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..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,10 +38,10 @@ .button.plugins-browser-list__select-all { float: right; - padding-right: 16px; } .plugins-browser-list__elements { min-height: 50px; overflow: hidden; // lazy clearfix + padding: 0; } diff --git a/client/notices/global-notices.jsx b/client/notices/global-notices.jsx new file mode 100644 index 0000000000000..e71f87cb6903d --- /dev/null +++ b/client/notices/global-notices.jsx @@ -0,0 +1,87 @@ +/** + * External Dependencies + */ +import React from 'react'; +import classNames from 'classnames'; +import debugModule from 'debug'; +import { connect } from 'react-redux'; +import noticeActionCreators from 'state/notices/actionCreators' + +/** + * Internal Dependencies + */ +import Notice from 'components/notice'; +const debug = debugModule( 'calypso:global-notices' ); + +const GlobalNotices = React.createClass( { + + propTypes: { + id: React.PropTypes.string, + forcePinned: React.PropTypes.bool + }, + + getInitialState() { + return { pinned: this.props.forcePinned }; + }, + + componentDidMount() { + if ( ! this.props.forcePinned ) { + window.addEventListener( 'scroll', this.updatePinnedState ); + } + }, + + componentDidUpdate( prevProps ) { + if ( this.props.forcePinned && ! prevProps.forcePinned ) { + window.removeEventListener( 'scroll', this.updatePinnedState ); + this.setState( { pinned: true } ); + } else if ( ! this.props.forcePinned && prevProps.forcePinned ) { + window.addEventListener( 'scroll', this.updatePinnedState ); + this.updatePinnedState(); + } + }, + + componentWillUnmount() { + window.removeEventListener( 'scroll', this.updatePinnedState ); + }, + + updatePinnedState() { + this.setState( { pinned: window.scrollY > 0 } ); + }, + + render() { + const noticesList = this.props.notices.map( function( notice, index ) { + return ( + + + ); + }, this ); + + if ( ! this.props.notices.length ) { + return null; + } + return ( +
+
+ { noticesList } +
+ { this.state.pinned && ! this.props.forcePinned + ?
+ : null } +
+ ); + } +} ); + +export default connect( + ( state ) => { + return { + notices: state.notices.items + }; + }, + noticeActionCreators +)( GlobalNotices ); diff --git a/client/state/index.js b/client/state/index.js index 52cbab6b5a1f7..e278602f2e7c0 100644 --- a/client/state/index.js +++ b/client/state/index.js @@ -10,6 +10,7 @@ import { createStore, applyMiddleware, combineReducers } from 'redux'; import sharing from './sharing/reducer'; import sites from './sites/reducer'; import ui from './ui/reducer'; +import notices from './notices/reducers'; /** * Module variables @@ -17,7 +18,8 @@ import ui from './ui/reducer'; const reducer = combineReducers( { sharing, sites, - ui + ui, + notices } ); export function createReduxStore() { diff --git a/client/state/notices/action-types.js b/client/state/notices/action-types.js new file mode 100644 index 0000000000000..48b89db39dc1c --- /dev/null +++ b/client/state/notices/action-types.js @@ -0,0 +1,2 @@ +export const NEW_NOTICE = 'NEW_NOTICE'; +export const REMOVE_NOTICE = 'REMOVE_NOTICE'; diff --git a/client/state/notices/actionCreators.js b/client/state/notices/actionCreators.js new file mode 100644 index 0000000000000..d3e3056d0b84e --- /dev/null +++ b/client/state/notices/actionCreators.js @@ -0,0 +1,27 @@ +import { createNoticeAction, removeNoticeAction } from './actions'; + +export default function( dispatch ) { + function createNotice( type, text, options ) { + var action = createNoticeAction( type, text, options ); + + if ( action.duration > 0 ) { + setTimeout( () => { + dispatch( removeNoticeAction( action.noticeId ) ); + }, action.duration ); + } + + dispatch( action ); + } + + return { + successNotice: ( text, options ) => { + createNotice( 'is-success', text, options ); + }, + errorNotice: ( text, options ) => { + createNotice( 'is-error', text, options ); + }, + removeNotice: ( noticeId ) => { + dispatch( removeNoticeAction( noticeId ) ); + } + }; +} diff --git a/client/state/notices/actions.js b/client/state/notices/actions.js new file mode 100644 index 0000000000000..218969c807f18 --- /dev/null +++ b/client/state/notices/actions.js @@ -0,0 +1,27 @@ +/** + * Internal dependencies + */ +import { + NEW_NOTICE, + REMOVE_NOTICE +} from './action-types'; + +import { uniqueId } from 'lodash'; + +export function createNoticeAction( status, text, options = {} ) { + return { + noticeId: uniqueId(), + duration: options.duration, + showDismiss: ( typeof options.showDismiss === 'boolean' ? options.showDismiss : true ), + type: NEW_NOTICE, + status: status, + text: text + }; +} + +export function removeNoticeAction( noticeId ) { + return { + noticeId: noticeId, + type: REMOVE_NOTICE + }; +} diff --git a/client/state/notices/reducers.js b/client/state/notices/reducers.js new file mode 100644 index 0000000000000..5208a60aa6126 --- /dev/null +++ b/client/state/notices/reducers.js @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import { combineReducers } from 'redux'; +import { filter } from 'lodash'; + +/** + * Internal dependencies + */ +import { + NEW_NOTICE, + REMOVE_NOTICE +} from './action-types'; + +/** + * 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 items( state = [], action ) { + switch ( action.type ) { + case NEW_NOTICE: + state = [ action, ...state ]; + break; + case REMOVE_NOTICE: + state = filter( state, ( notice ) => ( notice.noticeId !== action.noticeId ) ); + break; + } + + return state; +} + +export default combineReducers( { + items +} ); diff --git a/shared/components/gridicon/README.md b/shared/components/gridicon/README.md index 1c0f9373d96e4..18dc947b41662 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. +* `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.