Components: Export reusable components to a separate file + global (https://github.com/woocommerce/woocommerce-admin/pull/297)

* Importing all components from a root components file

* Update Ratings component to avoid circular dependencies, fix tests

* Export components on `wc.components`, use this for importing

* Move react-dates initialize to the components file

* Push query changes to history

Fixes an issues where native a links do not update the query in `history`

* Update test config for new @woocommerce/components path

* Update chart components import

* Merge simple/complex & alphabetize by first exported name

* Add a readme with info about how to call these components
This commit is contained in:
Kelly Dwan 2018-08-20 17:18:13 -04:00 committed by GitHub
parent 11524e19e1
commit dec97d178e
29 changed files with 210 additions and 126 deletions

View File

@ -9,7 +9,7 @@ import { Component, Fragment } from '@wordpress/element';
* Internal dependencies
*/
import Header from 'layout/header';
import { SummaryList, SummaryNumber } from 'components/summary';
import { SummaryList, SummaryNumber } from '@woocommerce/components';
export default class extends Component {
render() {

View File

@ -13,10 +13,9 @@ import { partial } from 'lodash';
/**
* Internal dependencies
*/
import Card from 'components/card';
import { Card, ReportFilters } from '@woocommerce/components';
import { filters, filterPaths, advancedFilterConfig } from './constants';
import Header from 'layout/header/index';
import { ReportFilters } from 'components/filters';
import './style.scss';
class OrdersReport extends Component {

View File

@ -10,7 +10,7 @@ import { Component, Fragment } from '@wordpress/element';
*/
import { filterPaths, filters } from './constants';
import Header from 'layout/header';
import { ReportFilters } from 'components/filters';
import { ReportFilters } from '@woocommerce/components';
import './style.scss';
export default class extends Component {

View File

@ -11,15 +11,18 @@ import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import Card from 'components/card';
import Chart from 'components/chart';
import {
Card,
Chart,
ReportFilters,
SummaryList,
SummaryNumber,
TableCard,
} from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency';
import { getAdminLink, updateQueryString } from 'lib/nav-utils';
import { getReportData } from 'lib/swagger';
import Header from 'layout/header';
import { ReportFilters } from 'components/filters';
import { SummaryList, SummaryNumber } from 'components/summary';
import { TableCard } from 'components/table';
// Mock data until we fetch from an API
import rawData from './mock-data';

View File

@ -0,0 +1,38 @@
Components
==========
This folder contains the WooCommerce-created components. These are exported onto a global, `wc.components`, for general use.
## How to use:
For any files not imported into `components/index.js` (`analytics/*`, `layout/*`, `dashboard/*`, etc), we can use `import { Card, etc … } from @woocommerce/components`.
For any `component/*` files, we should import from component-specific paths, not from `component/index.js`, to prevent circular dependencies. See `components/card/index.js` for an example.
```jsx
import { Card, Link } from '@woocommerce/components';
render: function() {
return (
<Card title="Card demo">
Card content with an <Link href="/">example link.</Link>
</Card>
);
}
```
## For external development
External developers will need to enqueue the components library, `wc-components`, and then can access them from the global.
```jsx
const { Card, Link } = wc.components;
render: function() {
return (
<Card title="Card demo">
Card content with an <Link href="/">example link.</Link>
</Card>
);
}
```

View File

@ -10,7 +10,7 @@ import PropTypes from 'prop-types';
* Internal dependencies
*/
import './style.scss';
import { EllipsisMenu } from '../ellipsis-menu';
import { EllipsisMenu } from 'components/ellipsis-menu';
import { H, Section } from 'layout/section';
class Card extends Component {

View File

@ -9,7 +9,7 @@ import { Component, Fragment } from '@wordpress/element';
* Internal dependencies
*/
import Card from 'components/card';
import Chart from 'components/chart';
import Chart from './index';
import dummyOrders from './test/fixtures/dummy';
class WidgetCharts extends Component {

View File

@ -14,7 +14,6 @@ import classnames from 'classnames';
import ComparePeriods from './compare-periods';
import { DateRange } from 'components/calendar';
import { H, Section } from 'layout/section';
import Link from 'components/link';
import PresetPeriods from './preset-periods';
const isMobileViewport = () => window.innerWidth < 782;
@ -45,7 +44,7 @@ class DatePickerContent extends Component {
before,
onUpdate,
onClose,
getUpdatePath,
onSelect,
isValidSelection,
resetCustomValues,
focusedInput,
@ -127,13 +126,13 @@ class DatePickerContent extends Component {
</Button>
) }
{ isValidSelection( selectedTab ) ? (
<Link
className="woocommerce-filters-date__button components-button is-button is-primary"
href={ getUpdatePath( selectedTab ) }
onClick={ onClose }
<Button
className="woocommerce-filters-date__button"
onClick={ onSelect( selectedTab, onClose ) }
isPrimary
>
{ __( 'Update', 'wc-admin' ) }
</Link>
</Button>
) : (
<Button className="woocommerce-filters-date__button" isPrimary disabled>
{ __( 'Update', 'wc-admin' ) }
@ -155,7 +154,7 @@ DatePickerContent.propTypes = {
compare: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
getUpdatePath: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired,
resetCustomValues: PropTypes.func.isRequired,
focusedInput: PropTypes.string,
afterText: PropTypes.string,

View File

@ -12,7 +12,7 @@ import { Dropdown } from '@wordpress/components';
import DatePickerContent from './content';
import DropdownButton from 'components/dropdown-button';
import { getCurrentDates, getDateParamsFromQuery, isoDateFormat } from 'lib/date';
import { getNewPath, getQuery } from 'lib/nav-utils';
import { getQuery, updateQueryString } from 'lib/nav-utils';
import './style.scss';
const shortDateFormat = __( 'MM/DD/YYYY', 'wc-admin' );
@ -23,7 +23,7 @@ class DatePicker extends Component {
this.state = this.getResetState();
this.update = this.update.bind( this );
this.getUpdatePath = this.getUpdatePath.bind( this );
this.onSelect = this.onSelect.bind( this );
this.isValidSelection = this.isValidSelection.bind( this );
this.resetCustomValues = this.resetCustomValues.bind( this );
}
@ -47,17 +47,20 @@ class DatePicker extends Component {
this.setState( update );
}
getUpdatePath( selectedTab ) {
const { period, compare, after, before } = this.state;
const data = {
period: 'custom' === selectedTab ? 'custom' : period,
compare,
onSelect( selectedTab, onClose ) {
return event => {
const { period, compare, after, before } = this.state;
const data = {
period: 'custom' === selectedTab ? 'custom' : period,
compare,
};
if ( 'custom' === selectedTab ) {
data.after = after ? after.format( isoDateFormat ) : '';
data.before = before ? before.format( isoDateFormat ) : '';
}
updateQueryString( data );
onClose( event );
};
if ( 'custom' === selectedTab ) {
data.after = after ? after.format( isoDateFormat ) : '';
data.before = before ? before.format( isoDateFormat ) : '';
}
return getNewPath( data );
}
getButtonLabel() {
@ -122,7 +125,7 @@ class DatePicker extends Component {
before={ before }
onUpdate={ this.update }
onClose={ onClose }
getUpdatePath={ this.getUpdatePath }
onSelect={ this.onSelect }
isValidSelection={ this.isValidSelection }
resetCustomValues={ this.resetCustomValues }
focusedInput={ focusedInput }

View File

@ -14,8 +14,7 @@ import PropTypes from 'prop-types';
*/
import AnimationSlider from 'components/animation-slider';
import DropdownButton from 'components/dropdown-button';
import { getNewPath, getQuery } from 'lib/nav-utils';
import Link from 'components/link';
import { getQuery, updateQueryString } from 'lib/nav-utils';
import './style.scss';
export const DEFAULT_FILTER = 'all';
@ -30,7 +29,6 @@ class FilterPicker extends Component {
animate: null,
};
this.getSelectionPath = this.getSelectionPath.bind( this );
this.getSelectedFilter = this.getSelectedFilter.bind( this );
this.selectSubFilters = this.selectSubFilters.bind( this );
this.getVisibleFilters = this.getVisibleFilters.bind( this );
@ -42,10 +40,6 @@ class FilterPicker extends Component {
return query.filter || DEFAULT_FILTER;
}
getSelectionPath( filter ) {
return getNewPath( { filter: filter.value } );
}
getSelectedFilter() {
const { filters, filterPaths } = this.props;
const value = this.getFilterValue();
@ -111,14 +105,15 @@ class FilterPicker extends Component {
);
}
const onClick = event => {
onClose( event );
updateQueryString( { filter: filter.value } );
};
return (
<Link
className="woocommerce-filters-filter__button components-button"
href={ this.getSelectionPath( filter ) }
onClick={ onClose }
>
<Button className="woocommerce-filters-filter__button" onClick={ onClick }>
{ filter.label }
</Link>
</Button>
);
}

View File

@ -0,0 +1,30 @@
/** @format */
/**
* External Dependencies
*/
// Turn on react-dates classes/styles, see https://github.com/airbnb/react-dates#initialize
import 'react-dates/initialize';
export { AdvancedFilters, DatePicker, FilterPicker, ReportFilters } from './filters';
export { default as AnimationSlider } from './animation-slider';
export { default as Card } from './card';
export { default as Chart } from './chart';
export { default as Count } from './count';
export { DateRange } from './calendar';
export { default as DropdownButton } from './dropdown-button';
export { EllipsisMenu, MenuItem, MenuTitle } from './ellipsis-menu';
export { default as Flag } from './flag';
export { default as Gravatar } from './gravatar';
export { default as Link } from './link';
export { default as OrderStatus } from './order-status';
export { default as Pagination } from './pagination';
export { default as ProductImage } from './product-image';
export { default as ProductRating } from './rating/product';
export { default as Rating } from './rating';
export { default as ReviewRating } from './rating/review';
export { default as SegmentedSelection } from './segmented-selection';
export { default as SplitButton } from './split-button';
export { SummaryList, SummaryNumber } from './summary';
export { TableCard, Table, TableSummary } from './table';
export { default as useFilters } from './higher-order/use-filters';

View File

@ -64,6 +64,4 @@ Rating.defaultProps = {
size: 18,
};
export { Rating };
export { default as ProductRating } from './product';
export { default as ReviewRating } from './review';
export default Rating;

View File

@ -3,21 +3,17 @@
/**
* External dependencies
*/
import { Component } from '@wordpress/element';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import { Rating } from './index';
import Rating from './index';
class ProductRating extends Component {
render() {
const { product, restOfProps } = this.props;
const rating = ( product && product.average_rating ) || 0;
return <Rating rating={ rating } { ...restOfProps } />;
}
}
const ProductRating = ( { product, ...props } ) => {
const rating = ( product && product.average_rating ) || 0;
return <Rating rating={ rating } { ...props } />;
};
ProductRating.propTypes = {
product: PropTypes.object.isRequired,

View File

@ -3,21 +3,17 @@
/**
* External dependencies
*/
import { Component } from '@wordpress/element';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import { Rating } from './index';
import Rating from './index';
class ReviewRating extends Component {
render() {
const { review, restOfProps } = this.props;
const rating = ( review && review.rating ) || 0;
return <Rating rating={ rating } { ...restOfProps } />;
}
}
const ReviewRating = ( { review, ...props } ) => {
const rating = ( review && review.rating ) || 0;
return <Rating rating={ rating } { ...props } />;
};
ReviewRating.propTypes = {
review: PropTypes.object.isRequired,

View File

@ -7,7 +7,9 @@ import { shallow } from 'enzyme';
/**
* Internal dependencies
*/
import { ReviewRating, ProductRating, Rating } from '../';
import Rating from '../';
import ProductRating from '../product';
import ReviewRating from '../review';
describe( 'Rating', () => {
test( 'should render the passed rating prop', () => {
@ -28,28 +30,22 @@ describe( 'Rating', () => {
describe( 'ReviewRating', () => {
test( 'should render rating based on review object', () => {
const rating = shallow(
<ReviewRating
review={ {
review: 'Nice T-shirt!',
rating: 1.5,
} }
/>
);
const review = {
review: 'Nice T-shirt!',
rating: 1.5,
};
const rating = shallow( <ReviewRating review={ review } /> );
expect( rating ).toMatchSnapshot();
} );
} );
describe( 'ProductRating', () => {
test( 'should render rating based on product object', () => {
const rating = shallow(
<ProductRating
product={ {
name: 'Test Product',
average_rating: 2.5,
} }
/>
);
const product = {
name: 'Test Product',
average_rating: 2.5,
};
const rating = shallow( <ProductRating product={ product } /> );
expect( rating ).toMatchSnapshot();
} );
} );

View File

@ -9,11 +9,11 @@ import { Component, Fragment } from '@wordpress/element';
* Internal dependencies
*/
import './style.scss';
import { Card } from '@woocommerce/components';
import ChartExample from 'components/chart/example';
import Header from 'layout/header';
import StorePerformance from './store-performance';
import TopSellingProducts from './top-selling-products';
import Chart from 'components/chart/example';
import Card from 'components/card';
export default class Dashboard extends Component {
render() {
@ -21,7 +21,7 @@ export default class Dashboard extends Component {
<Fragment>
<Header sections={ [ __( 'Dashboard', 'wc-admin' ) ] } />
<StorePerformance />
<Chart />
<ChartExample />
<div className="woocommerce-dashboard__columns">
<div>
<TopSellingProducts />

View File

@ -9,9 +9,14 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
import Card from 'components/card';
import { EllipsisMenu, MenuItem, MenuTitle } from 'components/ellipsis-menu';
import { SummaryList, SummaryNumber } from 'components/summary';
import {
Card,
EllipsisMenu,
MenuItem,
MenuTitle,
SummaryList,
SummaryNumber,
} from '@woocommerce/components';
import './style.scss';
class StorePerformance extends Component {

View File

@ -9,11 +9,10 @@ import { map } from 'lodash';
/**
* Internal dependencies
*/
import Card from 'components/card';
import { Card, Table } from '@woocommerce/components';
import { getAdminLink } from 'lib/nav-utils';
import { numberFormat } from 'lib/number';
import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency';
import { Table } from 'components/table';
import './style.scss';
// Mock data until we fetch from an API

View File

@ -6,7 +6,6 @@ import { APIProvider } from '@wordpress/components';
import { pick } from 'lodash';
import { render } from '@wordpress/element';
import { Provider as SlotFillProvider } from 'react-slot-fill';
import 'react-dates/initialize';
/**
* Internal dependencies

View File

@ -10,7 +10,7 @@ import { shallow } from 'enzyme';
* Internal dependencies
*/
import { ActivityCard } from '../';
import Gravatar from 'components/gravatar';
import { Gravatar } from '@woocommerce/components';
describe( 'ActivityCard', () => {
test( 'should have correct title', () => {

View File

@ -10,7 +10,7 @@ import PropTypes from 'prop-types';
* Internal dependencies
*/
import './style.scss';
import { EllipsisMenu } from 'components/ellipsis-menu';
import { EllipsisMenu } from '@woocommerce/components';
import { H } from 'layout/section';
class ActivityHeader extends Component {

View File

@ -11,7 +11,7 @@ import Gridicon from 'gridicons';
* Internal dependencies
*/
import './style.scss';
import Link from 'components/link';
import { Link } from '@woocommerce/components';
const ActivityOutboundLink = props => {
const { href, type, className, children, ...restOfProps } = props;

View File

@ -14,12 +14,16 @@ import { noop } from 'lodash';
import { ActivityCard } from '../activity-card';
import ActivityHeader from '../activity-header';
import ActivityOutboundLink from '../activity-outbound-link';
import { EllipsisMenu, MenuTitle, MenuItem } from 'components/ellipsis-menu';
import {
EllipsisMenu,
Gravatar,
Flag,
MenuTitle,
MenuItem,
OrderStatus,
} from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency';
import { getOrderRefundTotal } from 'lib/order-values';
import Gravatar from 'components/gravatar';
import Flag from 'components/flag';
import OrderStatus from 'components/order-status';
import { Section } from 'layout/section';
function OrdersPanel( { orders } ) {

View File

@ -13,12 +13,8 @@ import { noop } from 'lodash';
*/
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
import ActivityHeader from '../activity-header';
import Gravatar from 'components/gravatar';
import Link from 'components/link';
import ProductImage from 'components/product-image';
import { ReviewRating } from 'components/rating';
import { Gravatar, Link, ProductImage, ReviewRating, SplitButton } from '@woocommerce/components';
import { Section } from 'layout/section';
import SplitButton from 'components/split-button';
// TODO Pull proper data from the API
const demoReviews = [

View File

@ -8,7 +8,6 @@ import classnames from 'classnames';
import { decodeEntities } from '@wordpress/html-entities';
import { Fill } from 'react-slot-fill';
import { isArray } from 'lodash';
import Link from 'components/link';
import PropTypes from 'prop-types';
import ReactDom from 'react-dom';
@ -17,6 +16,7 @@ import ReactDom from 'react-dom';
*/
import './style.scss';
import ActivityPanel from '../activity-panel';
import { Link } from '@woocommerce/components';
class Header extends Component {
constructor() {

View File

@ -1,4 +1,10 @@
<?php
/**
* Register javascript & css files.
*
* @package WC_Admin
*/
/**
* Registers the JS & CSS for the admin and admin embed
*/
@ -14,10 +20,25 @@ function wc_admin_register_script() {
$css_entry = 'dist/css/index.css';
}
wp_register_script(
'wc-components',
wc_admin_url( 'dist/components.js' ),
[ 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-keycodes' ],
filemtime( wc_admin_dir_path( 'dist/components.js' ) ),
true
);
wp_register_style(
'wc-components',
wc_admin_url( 'dist/css/components.css' ),
[ 'wp-edit-blocks' ],
filemtime( wc_admin_dir_path( 'dist/css/components.css' ) )
);
wp_register_script(
WC_ADMIN_APP,
wc_admin_url( $js_entry ),
[ 'wp-blocks', 'wp-components', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-keycodes' ],
[ 'wc-components', 'wp-date', 'wp-html-entities', 'wp-keycodes' ],
filemtime( wc_admin_dir_path( $js_entry ) ),
true
);
@ -25,38 +46,38 @@ function wc_admin_register_script() {
wp_register_style(
WC_ADMIN_APP,
wc_admin_url( $css_entry ),
[ 'wp-edit-blocks' ],
[ 'wc-components' ],
filemtime( wc_admin_dir_path( $css_entry ) )
);
// Set up the text domain and translations
// Set up the text domain and translations.
$locale_data = gutenberg_get_jed_locale_data( 'wc-admin' );
$content = 'wp.i18n.setLocaleData( ' . json_encode( $locale_data ) . ', "wc-admin" );';
wp_add_inline_script( WC_ADMIN_APP, $content, 'before' );
$content = 'wp.i18n.setLocaleData( ' . json_encode( $locale_data ) . ', "wc-admin" );';
wp_add_inline_script( 'wc-components', $content, 'before' );
wp_enqueue_script( 'wp-api' );
// Settings and variables can be passed here for access in the app
// Settings and variables can be passed here for access in the app.
$settings = array(
'adminUrl' => admin_url(),
'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ),
'embedBreadcrumbs' => wc_admin_get_embed_breadcrumbs(),
'siteLocale' => esc_attr( get_bloginfo( 'language' ) ),
'currency' => wc_admin_currency_settings(),
'date' => array(
'adminUrl' => admin_url(),
'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ),
'embedBreadcrumbs' => wc_admin_get_embed_breadcrumbs(),
'siteLocale' => esc_attr( get_bloginfo( 'language' ) ),
'currency' => wc_admin_currency_settings(),
'date' => array(
'dow' => get_option( 'start_of_week', 0 ),
),
'orderStatuses' => wc_get_order_statuses(),
'siteTitle' => get_bloginfo( 'name' ),
'orderStatuses' => wc_get_order_statuses(),
'siteTitle' => get_bloginfo( 'name' ),
);
wp_add_inline_script(
WC_ADMIN_APP,
'var wcSettings = '. json_encode( $settings ) . ';',
'wc-components',
'var wcSettings = ' . json_encode( $settings ) . ';',
'before'
);
// Resets lodash to wp-admin's version of lodash
// Resets lodash to wp-admin's version of lodash.
wp_add_inline_script(
WC_ADMIN_APP,
'_.noConflict();',

View File

@ -8,7 +8,8 @@
],
"moduleDirectories": ["node_modules", "<rootDir>/client"],
"moduleNameMapper": {
"tinymce": "<rootDir>/tests/js/mocks/tinymce"
"tinymce": "<rootDir>/tests/js/mocks/tinymce",
"@woocommerce/components": "<rootDir>/client/components"
},
"setupFiles": [
"<rootDir>/node_modules/@wordpress/jest-preset-default/scripts/setup-globals.js",

View File

@ -14,6 +14,8 @@ global.wp = {
},
};
global.wc = {};
Object.defineProperty( global.wp, 'element', {
get: () => require( '@wordpress/element' ),
} );
@ -22,6 +24,10 @@ Object.defineProperty( global.wp, 'date', {
get: () => require( '@wordpress/date' ),
} );
Object.defineProperty( global.wc, 'components', {
get: () => require( '@woocommerce/components' ),
} );
global.wcSettings = {
adminUrl: 'https://vagrant.local/wp/wp-admin/',
locale: 'en-US',

View File

@ -9,6 +9,7 @@ const ExtractTextPlugin = require( 'extract-text-webpack-plugin' );
const NODE_ENV = process.env.NODE_ENV || 'development';
const externals = {
'@woocommerce/components': { this: [ 'wc', 'components' ] },
'@wordpress/blocks': { this: [ 'wp', 'blocks' ] },
'@wordpress/components': { this: [ 'wp', 'components' ] },
'@wordpress/compose': { this: [ 'wp', 'compose' ] },
@ -31,6 +32,7 @@ const webpackConfig = {
mode: NODE_ENV,
entry: {
index: './client/index.js',
components: './client/components/index.js',
embedded: './client/embedded.js',
},
output: {
@ -85,9 +87,7 @@ const webpackConfig = {
'gutenberg-components': path.resolve( __dirname, 'node_modules/@wordpress/components/src' ),
},
},
plugins: [
new ExtractTextPlugin( 'css/[name].css' ),
],
plugins: [ new ExtractTextPlugin( 'css/[name].css' ) ],
};
if ( webpackConfig.mode !== 'production' ) {