Merge pull request woocommerce/woocommerce-admin#2134 from woocommerce/add/dashboard-section-toggle

Dashboard: Section add, remove, move up, and move down
This commit is contained in:
Paul Sealock 2019-05-06 10:54:17 +12:00 committed by GitHub
commit b90f5ecb98
8 changed files with 367 additions and 102 deletions

View File

@ -0,0 +1,66 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Icon } from '@wordpress/components';
import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
import { MenuItem } from '@woocommerce/components';
import './style.scss';
class SectionControls extends Component {
constructor( props ) {
super( props );
this.onMoveUp = this.onMoveUp.bind( this );
this.onMoveDown = this.onMoveDown.bind( this );
}
onMoveUp() {
const { onMove, onToggle } = this.props;
onMove( -1 );
// Close the dropdown
onToggle();
}
onMoveDown() {
const { onMove, onToggle } = this.props;
onMove( 1 );
// Close the dropdown
onToggle();
}
render() {
const { onRemove, isFirst, isLast } = this.props;
if ( ! window.wcAdminFeatures[ 'dashboard/customizable' ] ) {
return null;
}
return (
<div className="woocommerce-section-controls">
{ ! isFirst && (
<MenuItem isClickable onInvoke={ this.onMoveUp }>
<Icon icon={ 'arrow-up-alt2' } label={ __( 'Move up' ) } />
{ __( 'Move up', 'woocommerce-admin' ) }
</MenuItem>
) }
{ ! isLast && (
<MenuItem isClickable onInvoke={ this.onMoveDown }>
<Icon icon={ 'arrow-down-alt2' } label={ __( 'Move Down' ) } />
{ __( 'Move Down', 'woocommerce-admin' ) }
</MenuItem>
) }
<MenuItem isClickable onInvoke={ onRemove }>
<Icon icon={ 'trash' } label={ __( 'Remove block' ) } />
{ __( 'Remove section', 'woocommerce-admin' ) }
</MenuItem>
</div>
);
}
}
export default SectionControls;

View File

@ -0,0 +1,14 @@
/** @format */
.woocommerce-section-controls {
border-top: $border-width solid $core-grey-light-500;
.dashicon {
margin: 0 $gap-smaller 0 0;
vertical-align: bottom;
}
.woocommerce-ellipsis-menu__item {
padding-bottom: 10px;
}
}

View File

@ -2,9 +2,11 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { applyFilters } from '@wordpress/hooks';
import { partial } from 'lodash';
import { IconButton, Icon, Dropdown, Button } from '@wordpress/components';
/**
* Internal dependencies
@ -16,7 +18,6 @@ import Section from './section';
import { ReportFilters, H } from '@woocommerce/components';
import StorePerformance from './store-performance';
// @todo Replace dashboard-charts, leaderboards, and store-performance sections as neccessary with customizable equivalents.
export default class CustomizableDashboard extends Component {
constructor( props ) {
super( props );
@ -25,20 +26,28 @@ export default class CustomizableDashboard extends Component {
{
key: 'store-performance',
component: StorePerformance,
title: __( 'Store Performance', 'woocommerce-admin' ),
title: __( 'Performance', 'woocommerce-admin' ),
isVisible: true,
icon: 'arrow-right-alt',
},
{
key: 'charts',
component: DashboardCharts,
title: __( 'Charts', 'woocommerce-admin' ),
isVisible: true,
icon: 'chart-bar',
},
{
key: 'leaderboards',
component: Leaderboards,
title: __( 'Leaderboards', 'woocommerce-admin' ),
isVisible: true,
icon: 'editor-ol',
},
] ),
};
this.onMove = this.onMove.bind( this );
}
onSectionTitleUpdate( updatedKey ) {
@ -57,15 +66,89 @@ export default class CustomizableDashboard extends Component {
};
}
toggleVisibility( key, onToggle ) {
return () => {
if ( onToggle ) {
// Close the dropdown before setting state so an action is not performed on an unmounted component.
onToggle();
}
this.setState( state => {
// When toggling visibility, place section at the end of the array.
const sections = [ ...state.sections ];
const index = sections.findIndex( s => key === s.key );
const toggledSection = sections.splice( index, 1 ).shift();
toggledSection.isVisible = ! toggledSection.isVisible;
sections.push( toggledSection );
return { sections };
} );
};
}
onMove( index, change ) {
const sections = [ ...this.state.sections ];
const movedSection = sections.splice( index, 1 ).shift();
sections.splice( index + change, 0, movedSection );
this.setState( { sections } );
}
renderAddMore() {
const { sections } = this.state;
const hiddenSections = sections.filter( section => false === section.isVisible );
if ( 0 === hiddenSections.length ) {
return null;
}
return (
<Dropdown
position="top center"
className="woocommerce-dashboard-section__add-more"
renderToggle={ ( { onToggle, isOpen } ) => (
<IconButton
onClick={ onToggle }
icon="plus-alt"
title={ __( 'Add more sections', 'woocommerce-admin' ) }
aria-expanded={ isOpen }
/>
) }
renderContent={ ( { onToggle } ) => (
<Fragment>
<H>{ __( 'Dashboard Sections', 'woocommerce-admin' ) }</H>
<div className="woocommerce-dashboard-section__add-more-choices">
{ hiddenSections.map( section => {
return (
<Button
key={ section.key }
onClick={ this.toggleVisibility( section.key, onToggle ) }
className="woocommerce-dashboard-section__add-more-btn"
title={ sprintf( __( 'Add %s section', 'woocommerce-admin' ), section.title ) }
>
<Icon icon={ section.icon } size={ 30 } />
<span className="woocommerce-dashboard-section__add-more-btn-title">
{ section.title }
</span>
</Button>
);
} ) }
</div>
</Fragment>
) }
/>
);
}
render() {
const { query, path } = this.props;
const { sections } = this.state;
const visibleSections = sections.filter( section => section.isVisible );
return (
<Fragment>
<H>{ __( 'Customizable Dashboard', 'woocommerce-admin' ) }</H>
<ReportFilters query={ query } path={ path } />
{ sections.map( section => {
{ visibleSections.map( ( section, index ) => {
return (
<Section
component={ section.component }
@ -74,9 +157,14 @@ export default class CustomizableDashboard extends Component {
path={ path }
query={ query }
title={ section.title }
onMove={ partial( this.onMove, index ) }
onRemove={ this.toggleVisibility( section.key ) }
isFirst={ 0 === index }
isLast={ visibleSections.length === index + 1 }
/>
);
} ) }
{ this.renderAddMore() }
</Fragment>
);
}

View File

@ -25,6 +25,7 @@ import ChartBlock from './block';
import { getChartFromKey, uniqCharts } from './config';
import withSelect from 'wc-api/with-select';
import './style.scss';
import SectionControls from 'dashboard/components/section-controls';
class DashboardCharts extends Component {
constructor( props ) {
@ -74,39 +75,57 @@ class DashboardCharts extends Component {
}
renderMenu() {
const { onTitleBlur, onTitleChange, titleInput } = this.props;
const {
onTitleBlur,
onTitleChange,
titleInput,
onMove,
onRemove,
isFirst,
isLast,
} = this.props;
const { hiddenChartKeys } = this.state;
return (
<EllipsisMenu
label={ __( 'Choose which charts to display and the section name', 'woocommerce-admin' ) }
>
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
<div className="woocommerce-ellipsis-menu__item">
<TextControl
label={ __( 'Section Title', 'woocommerce-admin' ) }
onBlur={ onTitleBlur }
onChange={ onTitleChange }
required
value={ titleInput }
label={ __( 'Choose which charts to display', 'woocommerce-admin' ) }
renderChildren={ ( { onToggle } ) => (
<Fragment>
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
<div className="woocommerce-ellipsis-menu__item">
<TextControl
label={ __( 'Section Title', 'woocommerce-admin' ) }
onBlur={ onTitleBlur }
onChange={ onTitleChange }
required
value={ titleInput }
/>
</div>
) }
<MenuTitle>{ __( 'Charts', 'woocommerce-admin' ) }</MenuTitle>
{ uniqCharts.map( chart => {
return (
<MenuItem
checked={ ! hiddenChartKeys.includes( chart.key ) }
isCheckbox
isClickable
key={ chart.key }
onInvoke={ this.toggle( chart.key ) }
>
{ __( `${ chart.label }`, 'woocommerce-admin' ) }
</MenuItem>
);
} ) }
<SectionControls
onToggle={ onToggle }
onMove={ onMove }
onRemove={ onRemove }
isFirst={ isFirst }
isLast={ isLast }
/>
</div>
</Fragment>
) }
<MenuTitle>{ __( 'Charts', 'woocommerce-admin' ) }</MenuTitle>
{ uniqCharts.map( chart => {
return (
<MenuItem
checked={ ! hiddenChartKeys.includes( chart.key ) }
isCheckbox
isClickable
key={ chart.key }
onInvoke={ this.toggle( chart.key ) }
>
{ __( `${ chart.label }`, 'woocommerce-admin' ) }
</MenuItem>
);
} ) }
</EllipsisMenu>
/>
);
}

View File

@ -20,6 +20,7 @@ import { EllipsisMenu, MenuItem, MenuTitle, SectionHeader } from '@woocommerce/c
*/
import Leaderboard from 'analytics/components/leaderboard';
import withSelect from 'wc-api/with-select';
import SectionControls from 'dashboard/components/section-controls';
import './style.scss';
class Leaderboards extends Component {
@ -54,7 +55,16 @@ class Leaderboards extends Component {
};
renderMenu() {
const { allLeaderboards, onTitleBlur, onTitleChange, titleInput } = this.props;
const {
allLeaderboards,
onTitleBlur,
onTitleChange,
titleInput,
onMove,
onRemove,
isFirst,
isLast,
} = this.props;
const { hiddenLeaderboardKeys, rowsPerTable } = this.state;
return (
@ -63,45 +73,53 @@ class Leaderboards extends Component {
'Choose which leaderboards to display and other settings',
'woocommerce-admin'
) }
>
<Fragment>
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
<div className="woocommerce-ellipsis-menu__item">
<TextControl
label={ __( 'Section Title', 'woocommerce-admin' ) }
onBlur={ onTitleBlur }
onChange={ onTitleChange }
required
value={ titleInput }
/>
</div>
) }
<MenuTitle>{ __( 'Leaderboards', 'woocommerce-admin' ) }</MenuTitle>
{ allLeaderboards.map( leaderboard => {
return (
<MenuItem
checked={ ! hiddenLeaderboardKeys.includes( leaderboard.id ) }
isCheckbox
isClickable
key={ leaderboard.id }
onInvoke={ this.toggle( leaderboard.id ) }
>
{ leaderboard.label }
</MenuItem>
);
} ) }
<SelectControl
className="woocommerce-dashboard__dashboard-leaderboards__select"
label={ <MenuTitle>{ __( 'Rows Per Table', 'woocommerce-admin' ) }</MenuTitle> }
value={ rowsPerTable }
options={ Array.from( { length: 20 }, ( v, key ) => ( {
v: key + 1,
label: key + 1,
} ) ) }
onChange={ this.setRowsPerTable }
/>
</Fragment>
</EllipsisMenu>
renderChildren={ ( { onToggle } ) => (
<Fragment>
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
<div className="woocommerce-ellipsis-menu__item">
<TextControl
label={ __( 'Section Title', 'woocommerce-admin' ) }
onBlur={ onTitleBlur }
onChange={ onTitleChange }
required
value={ titleInput }
/>
</div>
) }
<MenuTitle>{ __( 'Leaderboards', 'woocommerce-admin' ) }</MenuTitle>
{ allLeaderboards.map( leaderboard => {
return (
<MenuItem
checked={ ! hiddenLeaderboardKeys.includes( leaderboard.id ) }
isCheckbox
isClickable
key={ leaderboard.id }
onInvoke={ this.toggle( leaderboard.id ) }
>
{ leaderboard.label }
</MenuItem>
);
} ) }
<SelectControl
className="woocommerce-dashboard__dashboard-leaderboards__select"
label={ <MenuTitle>{ __( 'Rows Per Table', 'woocommerce-admin' ) }</MenuTitle> }
value={ rowsPerTable }
options={ Array.from( { length: 20 }, ( v, key ) => ( {
v: key + 1,
label: key + 1,
} ) ) }
onChange={ this.setRowsPerTable }
/>
<SectionControls
onToggle={ onToggle }
onMove={ onMove }
onRemove={ onRemove }
isFirst={ isFirst }
isLast={ isLast }
/>
</Fragment>
) }
/>
);
}

View File

@ -31,6 +31,7 @@ import {
SummaryNumber,
} from '@woocommerce/components';
import withSelect from 'wc-api/with-select';
import SectionControls from 'dashboard/components/section-controls';
import './style.scss';
class StorePerformance extends Component {
@ -66,7 +67,16 @@ class StorePerformance extends Component {
}
renderMenu() {
const { indicators, onTitleBlur, onTitleChange, titleInput } = this.props;
const {
indicators,
onTitleBlur,
onTitleChange,
titleInput,
onMove,
onRemove,
isFirst,
isLast,
} = this.props;
return (
<EllipsisMenu
@ -74,34 +84,44 @@ class StorePerformance extends Component {
'Choose which analytics to display and the section name',
'woocommerce-admin'
) }
>
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
<div className="woocommerce-ellipsis-menu__item">
<TextControl
label={ __( 'Section Title', 'woocommerce-admin' ) }
onBlur={ onTitleBlur }
onChange={ onTitleChange }
required
value={ titleInput }
renderChildren={ ( { onToggle } ) => (
<Fragment>
{ window.wcAdminFeatures[ 'dashboard/customizable' ] && (
<div className="woocommerce-ellipsis-menu__item">
<TextControl
label={ __( 'Section Title', 'woocommerce-admin' ) }
onBlur={ onTitleBlur }
onChange={ onTitleChange }
required
value={ titleInput }
/>
</div>
) }
<MenuTitle>{ __( 'Display Stats:', 'woocommerce-admin' ) }</MenuTitle>
{ indicators.map( ( indicator, i ) => {
const checked = ! this.state.hiddenIndicators.includes( indicator.stat );
return (
<MenuItem
checked={ checked }
isCheckbox
isClickable
key={ i }
onInvoke={ this.toggle( indicator.stat ) }
>
{ sprintf( __( 'Show %s', 'woocommerce-admin' ), indicator.label ) }
</MenuItem>
);
} ) }
<SectionControls
onToggle={ onToggle }
onMove={ onMove }
onRemove={ onRemove }
isFirst={ isFirst }
isLast={ isLast }
/>
</div>
</Fragment>
) }
<MenuTitle>{ __( 'Display Stats:', 'woocommerce-admin' ) }</MenuTitle>
{ indicators.map( ( indicator, i ) => {
const checked = ! this.state.hiddenIndicators.includes( indicator.stat );
return (
<MenuItem
checked={ checked }
isCheckbox
isClickable
key={ i }
onInvoke={ this.toggle( indicator.stat ) }
>
{ sprintf( __( 'Show %s', 'woocommerce-admin' ), indicator.label ) }
</MenuItem>
);
} ) }
</EllipsisMenu>
/>
);
}

View File

@ -28,3 +28,35 @@
.woocommerce-dashboard__widget-item {
flex: 1;
}
.woocommerce-dashboard-section__add-more {
margin: 0 auto;
width: 84px;
padding: $gap-large;
.components-popover__content {
padding: 0 $gap $gap-smaller;
}
}
.woocommerce-dashboard-section__add-more-choices {
display: flex;
justify-content: center;
}
.woocommerce-dashboard-section__add-more-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: $gap;
margin: $gap-smaller;
.dashicons-arrow-right-alt {
transform: rotate(-45deg);
}
}
.woocommerce-dashboard-section__add-more-btn-title {
color: $core-grey-dark-300;
padding-top: 8px;
}

View File

@ -12,8 +12,8 @@ import PropTypes from 'prop-types';
*/
class EllipsisMenu extends Component {
render() {
const { children, label } = this.props;
if ( ! children ) {
const { children, label, renderChildren } = this.props;
if ( ! children && ! renderChildren ) {
return null;
}
@ -33,8 +33,12 @@ class EllipsisMenu extends Component {
);
};
const renderContent = () => (
<NavigableMenu className="woocommerce-ellipsis-menu__content">{ children }</NavigableMenu>
// @todo Make all children rendered by render props so Dropdown args can be passed?
const renderContent = renderContentArgs => (
<NavigableMenu className="woocommerce-ellipsis-menu__content">
{ children && children }
{ renderChildren && renderChildren( renderContentArgs ) }
</NavigableMenu>
);
return (
@ -59,6 +63,10 @@ EllipsisMenu.propTypes = {
* A list of `MenuTitle`/`MenuItem` components
*/
children: PropTypes.node,
/**
* A list of `MenuTitle`/`MenuItem` components as a render prop. Arguments from Dropdown passed as function arguments.
*/
renderChildren: PropTypes.func,
};
export default EllipsisMenu;