diff --git a/_inc/client/admin.js b/_inc/client/admin.js index 5681d9bf1d5c6..028321da28256 100644 --- a/_inc/client/admin.js +++ b/_inc/client/admin.js @@ -64,11 +64,9 @@ function render() { - - diff --git a/_inc/client/appearance/index.jsx b/_inc/client/appearance/index.jsx deleted file mode 100644 index 69d514a9470c1..0000000000000 --- a/_inc/client/appearance/index.jsx +++ /dev/null @@ -1,153 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { connect } from 'react-redux'; -import FoldableCard from 'components/foldable-card'; -import Button from 'components/button'; -import Gridicon from 'components/gridicon'; -import { translate as __ } from 'i18n-calypso'; -import includes from 'lodash/includes'; -import analytics from 'lib/analytics'; - -/** - * Internal dependencies - */ -import QuerySite from 'components/data/query-site'; -import { - isModuleActivated as _isModuleActivated, - activateModule, - deactivateModule, - isActivatingModule, - isDeactivatingModule, - getModule as _getModule, - getModules -} from 'state/modules'; -import { getShowHolidaySnow } from 'state/settings'; -import { ModuleToggle } from 'components/module-toggle'; -import { AllModuleSettings } from 'components/module-settings/modules-per-tab-page'; -import { isUnavailableInDevMode } from 'state/connection'; -import { userCanManageModules } from 'state/initial-state'; -import Settings from 'components/settings'; - -export const Page = ( props ) => { - let { - toggleModule, - isModuleActivated, - isTogglingModule, - getModule, - showHolidaySnow - } = props, - isAdmin = props.userCanManageModules, - moduleList = Object.keys( props.moduleList ); - - var cards = [ - [ 'tiled-gallery', getModule( 'tiled-gallery' ).name, getModule( 'tiled-gallery' ).description, getModule( 'tiled-gallery' ).learn_more_button ], - [ 'photon', getModule( 'photon' ).name, getModule( 'photon' ).description, getModule( 'photon' ).learn_more_button ], - [ 'carousel', getModule( 'carousel' ).name, getModule( 'carousel' ).description, getModule( 'carousel' ).learn_more_button ], - [ 'widgets', getModule( 'widgets' ).name, getModule( 'widgets' ).description, getModule( 'widgets' ).learn_more_button ], - [ 'widget-visibility', getModule( 'widget-visibility' ).name, getModule( 'widget-visibility' ).description, getModule( 'widget-visibility' ).learn_more_button ], - [ 'custom-css', getModule( 'custom-css' ).name, getModule( 'custom-css' ).description, getModule( 'custom-css' ).learn_more_button ], - [ 'infinite-scroll', getModule( 'infinite-scroll' ).name, getModule( 'infinite-scroll' ).description, getModule( 'infinite-scroll' ).learn_more_button ], - [ 'minileven', getModule( 'minileven' ).name, getModule( 'minileven' ).description, getModule( 'minileven' ).learn_more_button ] - ].map( ( element ) => { - if ( ! includes( moduleList, element[0] ) ) { - return null; - } - var unavailableInDevMode = props.isUnavailableInDevMode( element[0] ), - toggle = ( - unavailableInDevMode ? __( 'Unavailable in Dev Mode' ) : - - ), - customClasses = unavailableInDevMode ? 'devmode-disabled' : ''; - - let moduleDescription = isModuleActivated( element[0] ) ? - : - // Render the long_description if module is deactivated -
; - - return ( - analytics.tracks.recordEvent( 'jetpack_wpa_settings_card_open', - { - card: element[0], - path: props.route.path - } - ) } - > - { moduleDescription } -
- -
-
- ); - } ); - - let holidaySnowCard = showHolidaySnow ? ( - : '' } - expandedSummary={ isAdmin ? : '' } - onOpen={ () => analytics.tracks.recordEvent( 'jetpack_wpa_settings_card_open', - { - card: 'holiday_snow', - path: props.route.path - } - ) } - > - - { __( 'Show falling snow on my blog from Dec 1st until Jan 4th.' ) } - - - ) : ''; - - return ( -
- - { cards } - { holidaySnowCard } -
- ); -}; - -function renderLongDescription( module ) { - // Rationale behind returning an object and not just the string - // https://facebook.github.io/react/tips/dangerously-set-inner-html.html - return { __html: module.long_description }; -} - -export default connect( - ( state ) => { - return { - isModuleActivated: ( module_name ) => _isModuleActivated( state, module_name ), - isTogglingModule: ( module_name ) => - isActivatingModule( state, module_name ) || isDeactivatingModule( state, module_name ), - getModule: ( module_name ) => _getModule( state, module_name ), - isUnavailableInDevMode: ( module_name ) => isUnavailableInDevMode( state, module_name ), - userCanManageModules: userCanManageModules( state ), - moduleList: getModules( state ), - showHolidaySnow: getShowHolidaySnow( state ) - }; - }, - ( dispatch ) => { - return { - toggleModule: ( module_name, activated ) => { - return ( activated ) - ? dispatch( deactivateModule( module_name ) ) - : dispatch( activateModule( module_name ) ); - } - }; - } -)( Page ); diff --git a/_inc/client/components/navigation-settings/index.jsx b/_inc/client/components/navigation-settings/index.jsx index 8847c6f66c2b7..c212b37ad1334 100644 --- a/_inc/client/components/navigation-settings/index.jsx +++ b/_inc/client/components/navigation-settings/index.jsx @@ -17,7 +17,11 @@ import Gridicon from 'components/gridicon'; /** * Internal dependencies */ -import { filterSearch } from 'state/search'; +import { + filterSearch, + focusSearch, + getSearchFocus as _getSearchFocus +} from 'state/search'; import { userCanManageModules as _userCanManageModules, userIsSubscriber as _userIsSubscriber @@ -31,13 +35,26 @@ export const NavigationSettings = React.createClass( { if ( currentHash.indexOf( 'search' ) === -1 ) { window.location.hash = 'search'; } + this.props.onSearchFocus && this.props.onSearchFocus( true ); }, onSearch( term ) { if ( term.length >= 3 ) { analytics.tracks.recordEvent( 'jetpack_wpa_search_term', { term: term.toLowerCase() } ); } + this.props.searchForTerm( trim( term || '' ).toLowerCase() ); + + if ( 0 === term.length ) { + + // Calling close handler to show what was previously shown to the user + this.onClose(); + } else { + + // Calling open handler in case the search was previously closed due to zero + // length search term + this.openSearch(); + } }, onClose: function() { @@ -47,6 +64,17 @@ export const NavigationSettings = React.createClass( { } }, + onBlur: function() { + let currentHash = window.location.hash; + this.props.onSearchFocus( false ); + + // If the user has navigated back a page, we discard the search term + // on blur + if ( currentHash.indexOf( 'search' ) === -1 ) { + this.props.searchForTerm( false ); + } + }, + maybeShowSearch: function() { if ( this.props.userCanManageModules ) { return ( @@ -58,7 +86,11 @@ export const NavigationSettings = React.createClass( { onSearchOpen={ this.openSearch } onSearch={ this.onSearch } onSearchClose={ this.onClose } - isOpen={ '/search' === this.props.route.path } + onBlur={ this.onBlur } + isOpen={ + '/search' === this.props.route.path + || this.props.searchHasFocus + } /> ); } @@ -143,12 +175,14 @@ export default connect( userCanManageModules: _userCanManageModules( state ), isSubscriber: _userIsSubscriber( state ), siteConnectionStatus: getSiteConnectionStatus( state ), - isModuleActivated: module => isModuleActivated( state, module ) + isModuleActivated: module => isModuleActivated( state, module ), + searchHasFocus: _getSearchFocus( state ) }; }, ( dispatch ) => { return { - searchForTerm: ( term ) => dispatch( filterSearch( term ) ) + searchForTerm: ( term ) => dispatch( filterSearch( term ) ), + onSearchFocus: ( hasFocus ) => dispatch( focusSearch( hasFocus ) ) } } )( NavigationSettings ); diff --git a/_inc/client/components/non-admin-view/index.jsx b/_inc/client/components/non-admin-view/index.jsx index dd8f75295f58f..9e94a3e1c7bc5 100644 --- a/_inc/client/components/non-admin-view/index.jsx +++ b/_inc/client/components/non-admin-view/index.jsx @@ -15,7 +15,6 @@ import { isModuleActivated as _isModuleActivated } from 'state/modules'; import Navigation from 'components/navigation'; import NavigationSettings from 'components/navigation-settings'; import AtAGlance from 'at-a-glance/index.jsx'; -import Engagement from 'engagement/index.jsx'; import Writing from 'writing/index.jsx'; import Apps from 'apps/index.jsx'; import { getSiteConnectionStatus } from 'state/connection'; @@ -46,12 +45,6 @@ const NonAdminView = React.createClass( { case '/apps': pageComponent = ; break; - case '/engagement': - if ( ! this.props.isSubscriber ) { - navComponent = ; - pageComponent = ; - } - break; case '/settings': case '/writing': if ( ! this.props.isSubscriber ) { diff --git a/_inc/client/discussion/index.jsx b/_inc/client/discussion/index.jsx index 5f52ef6b2cf50..a16597664a5bb 100644 --- a/_inc/client/discussion/index.jsx +++ b/_inc/client/discussion/index.jsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { getModule } from 'state/modules'; import { getSettings } from 'state/settings'; import { isDevMode, isUnavailableInDevMode } from 'state/connection'; +import { isModuleFound as _isModuleFound } from 'state/search'; import QuerySite from 'components/data/query-site'; import { Comments } from './comments'; import { Subscriptions } from './subscriptions'; @@ -24,16 +25,37 @@ export const Discussion = React.createClass( { isDevMode: this.props.isDevMode, isUnavailableInDevMode: this.props.isUnavailableInDevMode }; + + let found = { + comments: this.props.isModuleFound( 'comments' ), + subscriptions: this.props.isModuleFound( 'subscriptions' ) + }; + + if ( ! this.props.searchTerm && ! this.props.active ) { + return null; + } + + if ( ! found.comments && ! found.subscriptions ) { + return null; + } + + let commentsSettings = ( + + ); + let subscriptionsSettings = ( + + ); + return (
- - + { found.comments && commentsSettings } + { found.subscriptions && subscriptionsSettings }
); } @@ -45,7 +67,8 @@ export default connect( module: module_name => getModule( state, module_name ), settings: getSettings( state ), isDevMode: isDevMode( state ), - isUnavailableInDevMode: module_name => isUnavailableInDevMode( state, module_name ) + isUnavailableInDevMode: module_name => isUnavailableInDevMode( state, module_name ), + isModuleFound: ( module_name ) => _isModuleFound( state, module_name ), } } )( Discussion ); diff --git a/_inc/client/engagement/index.jsx b/_inc/client/engagement/index.jsx deleted file mode 100644 index 9c48174e6de72..0000000000000 --- a/_inc/client/engagement/index.jsx +++ /dev/null @@ -1,317 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { connect } from 'react-redux'; -import FoldableCard from 'components/foldable-card'; -import Button from 'components/button'; -import Gridicon from 'components/gridicon'; -import { translate as __ } from 'i18n-calypso'; -import includes from 'lodash/includes'; -import analytics from 'lib/analytics'; - -/** - * Internal dependencies - */ -import { - isModuleActivated as _isModuleActivated, - activateModule, - deactivateModule, - isActivatingModule, - isDeactivatingModule, - getModule as _getModule, - getModules -} from 'state/modules'; -import ProStatus from 'pro-status'; -import { ModuleToggle } from 'components/module-toggle'; -import { AllModuleSettings } from 'components/module-settings/modules-per-tab-page'; -import { isUnavailableInDevMode } from 'state/connection'; -import { - getSiteAdminUrl, - getSiteRawUrl, - isSitePublic, - getLastPostUrl, - userCanManageModules as _userCanManageModules -} from 'state/initial-state'; -import { getSitePlan } from 'state/site'; -import QuerySite from 'components/data/query-site'; -import ExternalLink from 'components/external-link'; - -export const Engagement = ( props ) => { - let { - toggleModule, - isModuleActivated, - isTogglingModule, - getModule - } = props, - isAdmin = props.userCanManageModules, - sitemapsDesc = getModule( 'sitemaps' ).description, - moduleList = Object.keys( props.moduleList ); - - if ( ! props.isSitePublic ) { - sitemapsDesc = - { sitemapsDesc } - {

- { __( 'Your site must be accessible by search engines for this feature to work properly. You can change this in {{a}}Reading Settings{{/a}}.', { - components: { - a: - } - } ) } -

} -
; - } - - /** - * Array of modules that directly map to a card for rendering - * @type {Array} - */ - let cards = [ - [ 'seo-tools', getModule( 'seo-tools' ).name, getModule( 'seo-tools' ).description, getModule( 'seo-tools' ).learn_more_button ], - [ 'wordads', getModule( 'wordads' ).name, getModule( 'wordads' ).description, getModule( 'wordads' ).learn_more_button ], - [ 'google-analytics', getModule( 'google-analytics' ).name, getModule( 'google-analytics' ).description, getModule( 'google-analytics' ).learn_more_button ], - [ 'stats', getModule( 'stats' ).name, getModule( 'stats' ).description, getModule( 'stats' ).learn_more_button ], - [ 'sharedaddy', getModule( 'sharedaddy' ).name, getModule( 'sharedaddy' ).description, getModule( 'sharedaddy' ).learn_more_button ], - [ 'publicize', getModule( 'publicize' ).name, getModule( 'publicize' ).description, getModule( 'publicize' ).learn_more_button ], - [ 'related-posts', getModule( 'related-posts' ).name, getModule( 'related-posts' ).description, getModule( 'related-posts' ).learn_more_button ], - [ 'likes', getModule( 'likes' ).name, getModule( 'likes' ).description, getModule( 'likes' ).learn_more_button ], - [ 'subscriptions', getModule( 'subscriptions' ).name, getModule( 'subscriptions' ).description, getModule( 'subscriptions' ).learn_more_button ], - [ 'gravatar-hovercards', getModule( 'gravatar-hovercards' ).name, getModule( 'gravatar-hovercards' ).description, getModule( 'gravatar-hovercards' ).learn_more_button ], - [ 'sitemaps', getModule( 'sitemaps' ).name, sitemapsDesc, getModule( 'sitemaps' ).learn_more_button ], - [ 'enhanced-distribution', getModule( 'enhanced-distribution' ).name, getModule( 'enhanced-distribution' ).description, getModule( 'enhanced-distribution' ).learn_more_button ], - [ 'verification-tools', getModule( 'verification-tools' ).name, getModule( 'verification-tools' ).description, getModule( 'verification-tools' ).learn_more_button ], - ], - nonAdminAvailable = [ 'publicize' ]; - // Put modules available to non-admin user at the top of the list. - if ( ! isAdmin ) { - let cardsCopy = cards.slice(); - cardsCopy.reverse().forEach( ( element ) => { - if ( includes( nonAdminAvailable, element[0] ) ) { - cards.unshift( element ); - } - } ); - cards = cards.filter( ( element, index ) => cards.indexOf( element ) === index ); - } - cards = cards.map( ( element ) => { - if ( ! includes( moduleList, element[0] ) ) { - return null; - } - - let unavailableInDevMode = props.isUnavailableInDevMode( element[0] ), - customClasses = unavailableInDevMode ? 'devmode-disabled' : '', - toggle = '', - adminAndNonAdmin = isAdmin || includes( nonAdminAvailable, element[0] ), - isPro = includes( [ 'seo-tools', 'wordads', 'google-analytics' ], element[0] ), - proProps = { - module: element[0], - configure_url: '' - }, - isModuleActive = isModuleActivated( element[0] ), - planLoaded = 'undefined' !== typeof props.sitePlan.product_slug, - hasBusiness = false, - hasPremiumOrBusiness = false, - wordAdsSubHeader = element[2]; - - hasBusiness = - planLoaded && - ( props.sitePlan.product_slug === 'jetpack_business' || - props.sitePlan.product_slug === 'jetpack_business_monthly' ); - - hasPremiumOrBusiness = - planLoaded && - ( props.sitePlan.product_slug === 'jetpack_premium' || - props.sitePlan.product_slug === 'jetpack_premium_monthly' || - props.sitePlan.product_slug === 'jetpack_business' || - props.sitePlan.product_slug === 'jetpack_business_monthly' ); - - if ( unavailableInDevMode ) { - toggle = __( 'Unavailable in Dev Mode' ); - } else if ( isAdmin ) { - if ( ( 'seo-tools' === element[0] && ! hasBusiness ) || - ( 'google-analytics' === element[0] && ! hasBusiness ) || - ( 'wordads' === element[0] && ! hasPremiumOrBusiness ) ) { - toggle = ; - } else { - toggle = - ; - - // Add text about TOS if inactive - if ( 'wordads' === element[0] && ! isModuleActive ) { - wordAdsSubHeader = - } - } - - if ( element[0] === 'google-analytics' && ! hasBusiness ) { - isModuleActive = false; - } - - if ( isPro ) { - // Add a "pro" button next to the header title - element[1] = - - { element[1] } - - ; - } - } - - let lastPostUrl = 'related-posts' === element[0] - ? { lastPostUrl: props.lastPostUrl } - : ''; - let moduleDescription = isModuleActive ? - : - // Render the long_description if module is deactivated -
; - - if ( element[0] === 'seo-tools' ) { - if ( 'undefined' === typeof props.sitePlan.product_slug && ! unavailableInDevMode ) { - proProps.configure_url = 'checking'; - } else if ( props.sitePlan.product_slug === 'jetpack_business' ) { - proProps.configure_url = isModuleActive - ? 'https://wordpress.com/settings/seo/' + props.siteRawUrl - : 'inactive'; - } - - moduleDescription = ; - } else if ( element[0] === 'google-analytics' ) { - proProps.configure_url = isModuleActive - ? 'https://wordpress.com/settings/analytics/' + props.siteRawUrl - : 'inactive'; - - moduleDescription = ; - } - - return adminAndNonAdmin ? ( - analytics.tracks.recordEvent( 'jetpack_wpa_settings_card_open', - { - card: element[0], - path: props.route.path - } - ) } - > - { - moduleDescription - } -
- -
- { - 'stats' === element[0] && isModuleActive - ?
- : '' - } - { - 'subscriptions' === element[0] && isModuleActive - ? - : '' - } - { - 'wordads' === element[0] && isModuleActive - ?
- - - { __( 'View your earnings' ) } - - -
- : '' - } - - - ) : false; - } ); - return ( -
- - { cards } -
- ); -}; - -export const WordAdsSubHeaderTos = React.createClass( { - render() { - return ( -
- ) - } -} ); - -function renderLongDescription( module ) { - // Rationale behind returning an object and not just the string - // https://facebook.github.io/react/tips/dangerously-set-inner-html.html - return { __html: module.long_description }; -} - -export default connect( - ( state ) => { - return { - isModuleActivated: ( module_name ) => _isModuleActivated( state, module_name ), - isTogglingModule: ( module_name ) => isActivatingModule( state, module_name ) || isDeactivatingModule( state, module_name ), - getModule: ( module_name ) => _getModule( state, module_name ), - isUnavailableInDevMode: ( module_name ) => isUnavailableInDevMode( state, module_name ), - siteRawUrl: getSiteRawUrl( state ), - siteAdminUrl: getSiteAdminUrl( state ), - isSitePublic: isSitePublic( state ), - sitePlan: getSitePlan( state ), - userCanManageModules: _userCanManageModules( state ), - moduleList: getModules( state ), - lastPostUrl: getLastPostUrl( state ) - }; - }, - ( dispatch ) => { - return { - toggleModule: ( module_name, activated ) => { - return ( activated ) - ? dispatch( deactivateModule( module_name ) ) - : dispatch( activateModule( module_name ) ); - } - }; - } -)( Engagement ); diff --git a/_inc/client/engagement/test/component.js b/_inc/client/engagement/test/component.js deleted file mode 100644 index 044edd04844cf..0000000000000 --- a/_inc/client/engagement/test/component.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { expect } from 'chai'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; - -/** - * Internal dependencies - */ -import { Engagement } from '../index'; - -describe( 'Engagement', () => { - - let testProps = { - toggleModule: () => true, - isModuleActivated: () => false, - isTogglingModule: () => false, - getModule: () => { - return { - 'name': 'SEO Tools', - 'description': 'Better results on search engines and social media.', - 'learn_more_button': 'https://jetpack.com/support/seo-tools/' - } - }, - isUnavailableInDevMode: () => false, - siteAdminUrl: 'https://example.org/wp-admin/', - siteRawUrl: 'https://example.org/', - isSitePublic: true, - sitePlan: { - 'product_slug': 'jetpack_premium' - }, - userCanManageModules: true, - moduleList: { - 'seo-tools': { - activated : false, - additional_search_queries: 'search engine optimization, social preview, meta description, custom title format', - auto_activate : 'No', - available : true, - changed : '', - configurable : false, - configure_url : 'https://example.org/wp-admin/admin.php?page=jetpack&configure=seo-tools', - deactivate : true, - description : 'Better results on search engines and social media.', - feature : ['Traffic', 'Jumpstart'], - free : true, - introduced : '4.4', - jumpstart_desc : 'Better results on search engines and social media.', - learn_more_button : 'https://jetpack.com/support/seo-tools/', - long_description : 'Better results on search engines and social media.', - module : 'seo-tools', - module_tags : ['Social', 'Appearance'], - name : 'SEO Tools', - options : [], - recommendation_order : 15, - requires_connection : true, - search_terms : 'search engine optimization, social preview, meta description, custom title format', - short_description : 'Better results on search engines and social media.', - sort : 35 - } - } - }; - - describe( 'Initially', () => { - - const wrapper = shallow( ); - - it( 'renders a card for SEO Tools', () => { - expect( wrapper.find( 'FoldableCard' ) ).to.have.length( 1 ); - expect( wrapper.find( 'FoldableCard' ).props().header.props.children[0] ).to.have.string( testProps.getModule().name ); - } ); - - it( "SEO Tools is not available in 'jetpack_premium' plan", () => { - expect( wrapper.find( 'FoldableCard' ).props().summary.type.displayName ).to.be.not.equal( 'ModuleToggle' ); - } ); - - } ); - - describe( 'if polling site plan from wpcom', () => { - - testProps = Object.assign( testProps, { - sitePlan: { - product_slug: undefined - } - } ); - - const wrapper = shallow( ); - - it( "don't display the toggle", () => { - expect( wrapper.find( 'FoldableCard' ).props().summary.type.displayName ).to.not.equal( 'ModuleToggle' ); - } ); - - } ); - - describe( "if site has 'jetpack_business' plan", () => { - - testProps = Object.assign( testProps, { - sitePlan: { - 'product_slug': 'jetpack_business' - } - } ); - - const wrapper = shallow( ); - let cardProps = wrapper.find( 'FoldableCard' ).props(); - - it( "SEO Tools is available in 'jetpack_business' plan", () => { - expect( cardProps.summary.type.displayName ).to.be.equal( 'ModuleToggle' ); - } ); - - it( 'always displays a PRO badge next to the title', () => { - expect( cardProps.header.props.children[1].type.displayName ).to.be.equal( 'Button' ); - } ); - - it( 'the PRO badge points to the Plans tab', () => { - expect( cardProps.header.props.children[1].props.href ).to.have.string( '#/plans' ); - } ); - - } ); - -} ); - diff --git a/_inc/client/main.jsx b/_inc/client/main.jsx index 9e929b12370f6..f113635a7df7e 100644 --- a/_inc/client/main.jsx +++ b/_inc/client/main.jsx @@ -15,6 +15,7 @@ import { translate as __ } from 'i18n-calypso'; import Masthead from 'components/masthead'; import Navigation from 'components/navigation'; import NavigationSettings from 'components/navigation-settings'; +import SearchableSettings from 'settings/index.jsx'; import JetpackConnect from 'components/jetpack-connect'; import JumpStart from 'components/jumpstart'; import { getJumpStartStatus, isJumpstarting } from 'state/jumpstart'; @@ -29,14 +30,9 @@ import { } from 'state/initial-state'; import { areThereUnsavedModuleOptions, clearUnsavedOptionFlag } from 'state/modules'; import { areThereUnsavedSettings, clearUnsavedSettingsFlag } from 'state/settings'; +import { getSearchTerm } from 'state/search'; import AtAGlance from 'at-a-glance/index.jsx'; -import Engagement from 'engagement/index.jsx'; -import Discussion from 'discussion'; -import Security from 'security/index.jsx'; -import Traffic from 'traffic'; -import Appearance from 'appearance/index.jsx'; -import Writing from 'writing/index.jsx'; import Apps from 'apps/index.jsx'; import Plans from 'plans/index.jsx'; import Footer from 'components/footer'; @@ -44,7 +40,6 @@ import SupportCard from 'components/support-card'; import NonAdminView from 'components/non-admin-view'; import JetpackNotices from 'components/jetpack-notices'; import AdminNotices from 'components/admin-notices'; -import SearchPage from 'search/index.jsx'; import analytics from 'lib/analytics'; import restApi from 'rest-api'; import { getTracksUserData } from 'state/initial-state'; @@ -108,9 +103,21 @@ const Main = React.createClass( { }, shouldComponentUpdate: function( nextProps ) { + + // A special case when user has just entered search mode and has not yet + // entered a search term + if ( + nextProps.route.path !== this.props.route.path + && '/search' === nextProps.route.path + && ! nextProps.searchTerm + ) { + return false; + } + return nextProps.siteConnectionStatus !== this.props.siteConnectionStatus || nextProps.jumpStartStatus !== this.props.jumpStartStatus || - nextProps.route.path !== this.props.route.path; + nextProps.route.path !== this.props.route.path || + nextProps.searchTerm !== this.props.searchTerm; }, componentWillReceiveProps( nextProps ) { @@ -166,6 +173,7 @@ const Main = React.createClass( { let pageComponent, navComponent = , settingsNav = ; + switch ( route ) { case '/dashboard': pageComponent = ; @@ -176,34 +184,20 @@ const Main = React.createClass( { case '/plans': pageComponent = ; break; + case '/settings': + case '/general': case '/engagement': - navComponent = settingsNav; - pageComponent = ; - break; - case '/discussion': - navComponent = settingsNav; - pageComponent = ; - break; case '/security': - navComponent = settingsNav; - pageComponent = ; - break; case '/traffic': - navComponent = settingsNav; - pageComponent = ; - break; - case '/appearance': - navComponent = settingsNav; - pageComponent = ; - break; - case '/settings': + case '/discussion': case '/writing': - navComponent = settingsNav; - pageComponent = ; - break; case '/search': navComponent = settingsNav; - pageComponent = ; + pageComponent = ; break; default: @@ -243,12 +237,13 @@ const Main = React.createClass( { export default connect( state => { - return { + return { jumpStartStatus: getJumpStartStatus( state ), isJumpstarting: isJumpstarting( state ), siteConnectionStatus: getSiteConnectionStatus( state ), siteRawUrl: getSiteRawUrl( state ), siteAdminUrl: getSiteAdminUrl( state ), + searchTerm: getSearchTerm( state ), apiRoot: getApiRootUrl( state ), apiNonce: getApiNonce( state ), tracksUserData: getTracksUserData( state ), diff --git a/_inc/client/scss/style.scss b/_inc/client/scss/style.scss index 59a84d6c44a67..04f33d26b096d 100644 --- a/_inc/client/scss/style.scss +++ b/_inc/client/scss/style.scss @@ -46,7 +46,4 @@ @import '../at-a-glance/style'; @import '../plans/style'; @import '../apps/style'; - -.dops-search__input[type="search"] { - width: 100%; -} +@import '../settings/style'; diff --git a/_inc/client/search/index.jsx b/_inc/client/search/index.jsx deleted file mode 100644 index 208be32d1a56b..0000000000000 --- a/_inc/client/search/index.jsx +++ /dev/null @@ -1,261 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { connect } from 'react-redux'; -import FoldableCard from 'components/foldable-card'; -import { ModuleToggle } from 'components/module-toggle'; -import forEach from 'lodash/forEach'; -import includes from 'lodash/includes'; -import Button from 'components/button'; -import Gridicon from 'components/gridicon'; -import Collection from 'components/search/search-collection.jsx'; -import { translate as __ } from 'i18n-calypso'; -import analytics from 'lib/analytics'; - -/** - * Internal dependencies - */ -import QuerySite from 'components/data/query-site'; -import { isUnavailableInDevMode } from 'state/connection'; -import { AllModuleSettings } from 'components/module-settings/modules-per-tab-page'; -import { - isModuleActivated as _isModuleActivated, - activateModule, - deactivateModule, - isActivatingModule, - isDeactivatingModule, - getModule as _getModule, - getModules as _getModules -} from 'state/modules'; -import { getSearchTerm } from 'state/search'; -import { - getSitePlan -} from 'state/site'; -import ProStatus from 'pro-status'; -import { - isFetchingPluginsData, - isPluginActive -} from 'state/site/plugins'; -import { getSiteRawUrl } from 'state/initial-state'; -import { WordAdsSubHeaderTos } from 'engagement' - -export const SearchResults = ( { - siteAdminUrl, - toggleModule, - isModuleActivated, - isTogglingModule, - getModule, - getModules, - searchTerm, - sitePlan, - unavailableInDevMode, - isFetchingPluginsData, - isPluginActive, - siteRawUrl - } ) => { - let modules = getModules(), - moduleList = [ - [ - 'scan', - __( 'Security Scanning' ), - __( 'Automatically scan your site for common threats and attacks.' ), - 'https://vaultpress.com/jetpack/', - 'security scan threat attacks pro scanning' // Extra search terms @todo make translatable - ], - [ - 'akismet', - 'Akismet', - __( 'Keep those spammers away!' ), - 'https://akismet.com/jetpack/', - 'spam security comments pro' - ], - [ - 'backups', - __( 'Site Backups' ), - __( 'Keep your site backed up!' ), - 'https://vaultpress.com/jetpack/', - 'backup restore pro security' - ] - ], - hasBusiness = false, - cards; - - forEach( modules, function( m ) { - 'vaultpress' !== m.module ? moduleList.push( [ - m.module, - getModule( m.module ).name, - getModule( m.module ).description, - getModule( m.module ).learn_more_button, - getModule( m.module ).long_description, - getModule( m.module ).search_terms, - getModule( m.module ).additional_search_queries, - getModule( m.module ).short_description, - getModule( m.module ).feature.toString() - ] ) : ''; - } ); - - if ( - undefined !== typeof sitePlan.product_slug - && ( - sitePlan.product_slug === 'jetpack_business' - || sitePlan.product_slug === 'jetpack_business_monthly' - ) - ) { - hasBusiness = true; - } - - cards = moduleList.map( ( element ) => { - const isPro = includes( [ 'scan', 'akismet', 'backups', 'seo-tools', 'google-analytics' ], element[0] ); - let proProps = {}, - isModuleActive = isModuleActivated( element[0] ), - unavailableDevMode = unavailableInDevMode( element[0] ), - toggle = unavailableDevMode ? __( 'Unavailable in Dev Mode' ) : ( - - ), - customClasses = unavailableDevMode ? 'devmode-disabled' : '', - wordAdsSubHeader = element[2]; - - if ( 'wordads' === element[0] && ! isModuleActive ) { - wordAdsSubHeader = - } - - if ( isPro ) { - proProps = { - module: element[0], - configure_url: '' - }; - - if ( ( - 'videopress' !== element[0] - || - 'seo-tools' !== element[0] - || ( - 'seo-tools' === element[0] - && ! hasBusiness - ) ) - && ( - 'google-analytics' !== element[0] - || ( 'google-analytics' === element[0] && ! hasBusiness ) - ) - ) { - toggle = ; - } - - // Add a "pro" button next to the header title - element[1] = - { element[1] } - - ; - - // Set proper .configure_url - if ( ! isFetchingPluginsData ) { - if ( 'akismet' === element[0] && isPluginActive( 'akismet/akismet.php' ) ) { - proProps.configure_url = siteAdminUrl + 'admin.php?page=akismet-key-config'; - } else if ( ( 'scan' === element[0] || 'backups' === element[0] ) && isPluginActive( 'vaultpress/vaultpress.php' ) ) { - proProps.configure_url = 'https://dashboard.vaultpress.com/'; - } - } - } - - if ( 'videopress' === element[0] ) { - if ( ! sitePlan || 'jetpack_free' === sitePlan.product_slug || /jetpack_personal*/.test( sitePlan.product_slug ) ) { - toggle = ; - } - } - - if ( 1 === element.length ) { - return (

{ element[0] }

); - } - - return ( - /gm, '' ) } - subheader={ 'wordads' === element[0] ? wordAdsSubHeader : element[2] } - summary={ toggle } - expandedSummary={ toggle } - clickableHeaderText={ true } - onOpen={ () => analytics.tracks.recordEvent( 'jetpack_wpa_settings_card_open', - { - card: element[0], - path: '/search' - } - ) } - > - { - isModuleActive || isPro ? - : - // Render the long_description if module is deactivated -
- } -
-
- -
- - ); - } ); - - return ( -
- - - { cards } - -
- ); -}; - -function renderLongDescription( module ) { - // Rationale behind returning an object and not just the string - // https://facebook.github.io/react/tips/dangerously-set-inner-html.html - return { __html: module.long_description }; -} - -export default connect( - ( state ) => { - return { - isModuleActivated: ( module_name ) => _isModuleActivated( state, module_name ), - isTogglingModule: ( module_name ) => isActivatingModule( state, module_name ) || isDeactivatingModule( state, module_name ), - getModule: ( module_name ) => _getModule( state, module_name ), - getModules: () => _getModules( state ), - searchTerm: () => getSearchTerm( state ), - sitePlan: getSitePlan( state ), - unavailableInDevMode: ( module_name ) => isUnavailableInDevMode( state, module_name ), - isFetchingPluginsData: isFetchingPluginsData( state ), - isPluginActive: ( plugin_slug ) => isPluginActive( state, plugin_slug ), - siteRawUrl: getSiteRawUrl( state ) - }; - }, - ( dispatch ) => { - return { - toggleModule: ( module_name, activated ) => { - return ( activated ) - ? dispatch( deactivateModule( module_name ) ) - : dispatch( activateModule( module_name ) ); - } - }; - } -)( SearchResults ); diff --git a/_inc/client/security/index.jsx b/_inc/client/security/index.jsx index 654ccb950bf7b..d659fb5e96e39 100644 --- a/_inc/client/security/index.jsx +++ b/_inc/client/security/index.jsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { getModule } from 'state/modules'; import { getSettings } from 'state/settings'; import { isDevMode, isUnavailableInDevMode } from 'state/connection'; +import { isModuleFound as _isModuleFound } from 'state/search'; import QuerySite from 'components/data/query-site'; import { BackupsScan } from './backups-scan'; import { Antispam } from './antispam'; @@ -26,21 +27,47 @@ export const Security = React.createClass( { isDevMode: this.props.isDevMode, isUnavailableInDevMode: this.props.isUnavailableInDevMode }; + + let found = { + protect: this.props.isModuleFound( 'protect' ), + sso: this.props.isModuleFound( 'sso' ) + }; + + if ( ! this.props.searchTerm && ! this.props.active ) { + return null; + } + + if ( ! found.sso && ! found.protect ) { + return null; + } + + let backupSettings = ( + + ); + let akismetSettings = ( + + ); + let protectSettings = ( + + ); + let ssoSettings = ( + + ); return (
- - - - + { ( found.protect || found.sso ) && backupSettings } + { ( found.protect || found.sso ) && akismetSettings } + { found.protect && protectSettings } + { found.sso && ssoSettings }
); } @@ -52,7 +79,8 @@ export default connect( module: module_name => getModule( state, module_name ), settings: getSettings( state ), isDevMode: isDevMode( state ), - isUnavailableInDevMode: module_name => isUnavailableInDevMode( state, module_name ) + isUnavailableInDevMode: module_name => isUnavailableInDevMode( state, module_name ), + isModuleFound: ( module_name ) => _isModuleFound( state, module_name ), } } )( Security ); diff --git a/_inc/client/settings/index.jsx b/_inc/client/settings/index.jsx new file mode 100644 index 0000000000000..318e06660e67f --- /dev/null +++ b/_inc/client/settings/index.jsx @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import React from 'react'; +import { translate as __ } from 'i18n-calypso'; + +/** + * Internal dependencies + */ +import Discussion from 'discussion'; +import Security from 'security/index.jsx'; +import Traffic from 'traffic'; +import Writing from 'writing/index.jsx'; + +export default React.createClass( { + displayName: 'SearchableSettings', + + render() { + var commonProps = { + route: this.props.route, + searchTerm: this.props.searchTerm + }; + + return ( +
+
+ { false !== commonProps.searchTerm + ? __( + 'No search results found for %(term)s', + { + args: { + term: commonProps.searchTerm + } + } + ) + : __( 'Enter a search term to find settings or close search.' ) + } +
+ + + + +
+ ); + } +} ); + diff --git a/_inc/client/settings/style.scss b/_inc/client/settings/style.scss new file mode 100644 index 0000000000000..56f40542ffbfb --- /dev/null +++ b/_inc/client/settings/style.scss @@ -0,0 +1,13 @@ +.dops-search__input[type="search"] { + width: 100%; +} + +.jp-settings-container .jp-no-results { + display: none; + font-size: rem( 14px ); + line-height: 1.5; + + &:last-of-type { + display: inherit; + } +} diff --git a/_inc/client/state/action-types.js b/_inc/client/state/action-types.js index 1bbd1865fabbd..dd7c4cdbc8d54 100644 --- a/_inc/client/state/action-types.js +++ b/_inc/client/state/action-types.js @@ -96,3 +96,5 @@ export const JETPACK_PLUGINS_DATA_FETCH_RECEIVE = 'JETPACK_PLUGINS_DATA_FETCH_RE export const JETPACK_PLUGINS_DATA_FETCH_FAIL = 'JETPACK_PLUGINS_DATA_FETCH_FAIL'; export const JETPACK_SEARCH_TERM = 'JETPACK_SEARCH_TERM'; +export const JETPACK_SEARCH_FOCUS = 'JETPACK_SEARCH_FOCUS'; +export const JETPACK_SEARCH_BLUR = 'JETPACK_SEARCH_BLUR'; diff --git a/_inc/client/state/search/actions.js b/_inc/client/state/search/actions.js index 527b21db1d74d..f3d435429a639 100644 --- a/_inc/client/state/search/actions.js +++ b/_inc/client/state/search/actions.js @@ -2,7 +2,9 @@ * Internal dependencies */ import { - JETPACK_SEARCH_TERM + JETPACK_SEARCH_TERM, + JETPACK_SEARCH_FOCUS, + JETPACK_SEARCH_BLUR } from 'state/action-types'; export const filterSearch = ( term ) => { @@ -13,3 +15,17 @@ export const filterSearch = ( term ) => { } ); } }; + +export const focusSearch = ( hasFocus ) => { + return ( dispatch ) => { + if ( hasFocus ) { + dispatch( { + type: JETPACK_SEARCH_FOCUS + } ); + } else { + dispatch( { + type: JETPACK_SEARCH_BLUR + } ); + } + } +}; diff --git a/_inc/client/state/search/reducer.js b/_inc/client/state/search/reducer.js index b8f82fa4b3e32..6eace01539c9b 100644 --- a/_inc/client/state/search/reducer.js +++ b/_inc/client/state/search/reducer.js @@ -2,13 +2,17 @@ /** * External dependencies */ +import get from 'lodash/get'; +import find from 'lodash/find'; import { combineReducers } from 'redux'; /** * Internal dependencies */ import { - JETPACK_SEARCH_TERM + JETPACK_SEARCH_TERM, + JETPACK_SEARCH_FOCUS, + JETPACK_SEARCH_BLUR } from 'state/action-types'; const searchTerm = ( state = false, action ) => { @@ -21,8 +25,22 @@ const searchTerm = ( state = false, action ) => { } }; +const searchFocus = ( state = false, action ) => { + switch ( action.type ) { + case JETPACK_SEARCH_FOCUS: + return true; + + case JETPACK_SEARCH_BLUR: + return false; + + default: + return state; + } +}; + export const reducer = combineReducers( { - searchTerm + searchTerm, + searchFocus } ); /** @@ -34,3 +52,50 @@ export const reducer = combineReducers( { export function getSearchTerm( state ) { return state.jetpack.search.searchTerm; } + +/** + * Returns the Search Focus state + * + * @param {Object} state Global state tree + * @return {Boolean} Whether the search input has focus + */ +export function getSearchFocus( state ) { + return state.jetpack.search.searchFocus; +} + +/** + * Returns the module found status + * + * @param {Object} state Global state tree + * @param {String} module The module slug + * @return {Boolean} Whether the module should be in the search results + */ +export function isModuleFound( state, module ) { + let result = get( state, [ 'jetpack', 'modules', 'items' ], {} ); + + result = find( result, [ 'module', module ] ); + + if ( 'undefined' === typeof result ) { + return false; + } + + let text = [ + result.module, + result.name, + result.description, + result.learn_more_button, + result.long_description, + result.search_terms, + result.additional_search_queries, + result.short_description, + result.feature ? result.feature.toString() : '' + ].join( ' ' ); + + let searchTerm = get( state, [ 'jetpack', 'search', 'searchTerm' ], false ); + + if ( ! searchTerm ) { + return true; + } + + return text.toLowerCase().indexOf( searchTerm.toLowerCase() ) > -1; +} diff --git a/_inc/client/state/search/test/reducer.js b/_inc/client/state/search/test/reducer.js new file mode 100644 index 0000000000000..dc46d8ca0298b --- /dev/null +++ b/_inc/client/state/search/test/reducer.js @@ -0,0 +1,63 @@ +import { expect } from 'chai'; + +import { + reducer as searchReducer +} from '../reducer'; + +describe( 'Search reducer', () => { + + describe( 'term property', () => { + it( 'should get set on the respective event', () => { + const stateIn = {}; + const action = { + type: 'JETPACK_SEARCH_TERM', + term: 'Something' + }; + let stateOut = searchReducer( stateIn, action ); + expect( stateOut.searchTerm ).to.equal( action.term ); + } ); + + it( 'should not change on any other events', () => { + const stateIn = { + searchTerm: 'initial state' + }; + + const action = { + type: 'JETPACK_SOME_EVENT', + term: 'This should not get in' + }; + let stateOut = searchReducer( stateIn, action ); + expect( stateOut.searchTerm ).to.equal( stateIn.searchTerm ); + } ); + } ); + + describe( 'focus property', () => { + it( 'should get set on the respective event', () => { + const stateIn = {}; + const action = { + type: 'JETPACK_SEARCH_FOCUS', + }; + let stateOut = searchReducer( stateIn, action ); + expect( stateOut.searchFocus ).to.be.true; + } ); + + it( 'should get unset on the respective event', () => { + const stateIn = {}; + const action = { + type: 'JETPACK_SEARCH_BLUR', + }; + let stateOut = searchReducer( stateIn, action ); + expect( stateOut.searchFocus ).to.be.false; + } ); + + it( 'should not change on any other events', () => { + const stateIn = {}; + + const action = { + type: 'JETPACK_SOME_EVENT' + }; + let stateOut = searchReducer( stateIn, action ); + expect( stateOut.searchFocus ).to.be.false; + } ); + } ); +} ); diff --git a/_inc/client/state/search/test/selectors.js b/_inc/client/state/search/test/selectors.js new file mode 100644 index 0000000000000..dd2cedfb0bf60 --- /dev/null +++ b/_inc/client/state/search/test/selectors.js @@ -0,0 +1,79 @@ +import { expect } from 'chai'; + +import { isModuleFound } from '../index'; + +describe( 'Module found selector', () => { + let state = {}; + + before( () => { + state = { + jetpack: { + modules: { + items: { + manage: { + module: 'manage', + name: 'Manage', + description: 'Description', + learn_more_button: 'Learn more', + long_description: 'Long Description', + search_terms: 'Search Terms', + additional_search_queries: 'Additional Queries', + short_description: 'So short you will be surprised by how it fits into one line', + feature: 'Some modules do not have it, can you believe it?' + } + } + }, + search: { + searchTerm: false + } + } + } + } ); + + describe( 'when there is no search term', () => { + it( 'returns true for every module', () => { + expect( isModuleFound( state, 'manage' ) ).to.be.true; + } ); + it( 'returns false for modules that do not exist', () => { + expect( isModuleFound( state, 'make-everything-fast' ) ).to.be.false; + expect( isModuleFound( state, 'take-over-the-world' ) ).to.be.false; + } ); + } ); + + describe( 'for an existing module', () => { + [ + 'manage', + 'Description', + 'Learn', + 'Long', + 'Terms', + 'TeRMs', // case sensitivity test + 'quer', + 'surprise', + 'believe' + ].map( function( term ) { + describe( 'for the term ' + term, () => { + it( 'should match', () => { + state.jetpack.search.searchTerm = term; + + expect( isModuleFound( state, 'manage' ) ).to.be.true; + } ); + } ); + } ); + + [ + 'nonexistent-slug', + 'Decscripton', + 'Hocus Pocus', + 'something else' + ].map( function( term ) { + describe( 'for the term ' + term, () => { + it( 'should not match', () => { + state.jetpack.search.searchTerm = term; + + expect( isModuleFound( state, 'manage' ) ).to.be.false; + } ); + } ); + } ); + } ); +} ); diff --git a/_inc/client/traffic/index.jsx b/_inc/client/traffic/index.jsx index ac74e4149eabc..1370f4ab82e98 100644 --- a/_inc/client/traffic/index.jsx +++ b/_inc/client/traffic/index.jsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { getModule } from 'state/modules'; import { getSettings } from 'state/settings'; import { isDevMode, isUnavailableInDevMode } from 'state/connection'; +import { isModuleFound as _isModuleFound } from 'state/search'; import QuerySite from 'components/data/query-site'; import { SEO } from './seo'; import { Ads } from './ads'; @@ -28,30 +29,71 @@ export const Traffic = React.createClass( { isDevMode: this.props.isDevMode, isUnavailableInDevMode: this.props.isUnavailableInDevMode }; + + let found = { + seo: this.props.isModuleFound( 'seo-tools' ), + ads: this.props.isModuleFound( 'wordads' ), + stats: this.props.isModuleFound( 'stats' ), + related: this.props.isModuleFound( 'related-posts' ), + verification: this.props.isModuleFound( 'verification-tools' ), + sitemaps: this.props.isModuleFound( 'sitemaps' ) + }; + + if ( ! this.props.searchTerm && ! this.props.active ) { + return null; + } + + if ( + ! found.seo + && ! found.ads + && ! found.stats + && ! found.related + && ! found.verification + && ! found.sitemaps + ) { + return null; + } + + let seoSettings = ( + + ); + let adSettings = ( + + ); + let statsSettings = ( + + ); + let relatedPostsSettings = ( + - - -