Docs Project: Add documentation parser + inline documentation (https://github.com/woocommerce/woocommerce-admin/pull/336)

* Switch all components to default exports

This ensures we can read all components for documentation generation (plus, standardizing is good)

* Add documentation to component file

* Fix table exports

* Move readme docs into inline docs

Includes updating new props, adding prop shapes

* Add doc-generation scripts to pull exported component docs into folder

* Remove key propType, causing react special-keys warning

* Fix proptype

* Update incorrect comment

* Remove template import, we can just use string concat

* Fix typo, update docs
This commit is contained in:
Kelly Dwan 2018-08-31 13:27:21 -04:00 committed by GitHub
parent 7711da472a
commit d36511479e
41 changed files with 1149 additions and 22 deletions

View File

@ -1 +1,2 @@
bin
bin/*
!bin/generate-docs

View File

@ -0,0 +1,73 @@
/** @format */
/* eslint-disable no-console */
/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );
const { parse } = require( 'react-docgen' );
/**
* Internal dependencies
*/
const { getDescription, getProps, getTitle } = require( './lib/formatting' );
const {
COMPONENTS_FOLDER,
deleteExistingDocs,
getExportedFileList,
getMdFileName,
getRealFilePaths,
writeTableOfContents,
} = require( './lib/file-system' );
const filePath = path.resolve( COMPONENTS_FOLDER, 'index.js' );
// Start by wiping the existing docs. **Change this if we end up manually editing docs**
deleteExistingDocs();
// Read components file to get a list of exported files, convert that to a list of absolute paths to public components.
const files = getRealFilePaths( getExportedFileList( filePath ) );
// Build the documentation by reading each file.
files.forEach( file => {
try {
const content = fs.readFileSync( file );
buildDocs( file, content );
} catch ( readErr ) {
console.warn( file, readErr );
}
} );
writeTableOfContents( files );
console.log( `Wrote docs for ${ files.length } files.` );
/**
* Parse each file's content & build up a markdown file.
*
* @param { string } fileName The absolute path of this file.
* @param { string } content Content of this file.
*/
function buildDocs( fileName, content ) {
try {
const docObject = parse( content );
const mdFileName = getMdFileName( fileName );
const markdown = generateMarkdown( docObject );
fs.appendFileSync( mdFileName, markdown );
} catch ( parseErr ) {
console.warn( fileName, parseErr );
}
}
/**
* Convert documentation object to a markdown string.
*
* @param { object } docObject The parsed documentation object.
* @return { string } Generated markdown.
*/
function generateMarkdown( docObject ) {
let markdownString = getTitle( docObject.displayName ) + '\n';
markdownString += getDescription( docObject.description ) + '\n';
markdownString += getProps( docObject.props );
return markdownString;
}

View File

@ -0,0 +1,166 @@
/** @format */
/**
* External dependencies
*/
const { isArray, uniq } = require( 'lodash' );
const fs = require( 'fs' );
const path = require( 'path' );
const recast = require( 'recast' );
const types = require( 'ast-types' );
const { namedTypes } = types;
/**
* Internal dependencies
*/
const { camelCaseDash } = require( './formatting' );
const COMPONENTS_FOLDER = path.resolve( __dirname, '../../../client/components/' );
const DOCS_FOLDER = path.resolve( __dirname, '../../../docs/components/' );
/**
* Remove all files in existing docs folder.
*/
function deleteExistingDocs() {
if ( ! isDirectory( DOCS_FOLDER ) ) {
fs.mkdirSync( DOCS_FOLDER );
return;
}
const files = fs.readdirSync( DOCS_FOLDER );
files.map( file => fs.unlinkSync( path.resolve( DOCS_FOLDER, file ) ) );
fs.rmdirSync( DOCS_FOLDER );
fs.mkdirSync( DOCS_FOLDER );
}
/**
* Get an array of files exported from in the given file
*
* @param { string } filePath The file to parse for exports.
* @return { string } Formatted string.
*/
function getExportedFileList( filePath ) {
const content = fs.readFileSync( filePath );
const ast = recast.parse( content );
const files = [];
types.visit( ast, {
// This method will be called for any node with .type "ExportNamedDeclaration":
visitExportNamedDeclaration: function( nodePath ) {
const { node } = nodePath;
if (
namedTypes.Literal.check( node.source ) &&
isArray( node.specifiers ) &&
namedTypes.ExportSpecifier.check( node.specifiers[ 0 ] )
) {
if ( -1 === node.source.value.indexOf( 'use-filters' ) ) {
files.push( node.source.value );
}
}
// Keep traversing this path…
this.traverse( nodePath );
},
} );
return files;
}
/**
* Return the markdown file name for a given component file.
*
* @param { string } filepath File path for this component.
* @param { boolean } absolute Whether to return full path (true) or just filename (false).
* @return { string } Markdown file name.
*/
function getMdFileName( filepath, absolute = true ) {
const fileParts = filepath.split( '/components/' );
if ( ! fileParts || ! fileParts[ 1 ] ) {
return;
}
const name = fileParts[ 1 ].split( '/' )[ 0 ];
if ( ! absolute ) {
return name + '.md';
}
return path.resolve( DOCS_FOLDER, name + '.md' );
}
/**
* Get an array of files exported from in the given file
*
* @param { array } files A list of files, presumably in the components directory.
* @param { string } basePath The absolute path to the components directory.
* @return { array } Updated array with absolute paths to all files.
*/
function getRealFilePaths( files, basePath = COMPONENTS_FOLDER ) {
files.sort();
return files.map( file => {
const fullPath = path.resolve( basePath, file );
if ( isFile( fullPath ) ) {
return fullPath;
}
if ( isFile( `${ fullPath }.js` ) ) {
return `${ fullPath }.js`;
}
if ( isFile( `${ fullPath }/index.js` ) ) {
return `${ fullPath }/index.js`;
}
const folderName = path.basename( fullPath );
if ( isFile( `${ fullPath }/${ folderName }.js` ) ) {
return `${ fullPath }/${ folderName }.js`;
}
return fullPath;
} );
}
/**
* Check if a directory exists and is not a file.
*
* @param { string } dir A directory path to test.
* @return { boolean } True if this path exists and is a directory.
*/
function isDirectory( dir ) {
if ( ! fs.existsSync( dir ) ) {
return false;
}
const stats = fs.statSync( dir );
return stats && stats.isDirectory();
}
/**
* Check if a file exists and is not a directory.
*
* @param { string } file A file path to test.
* @return { boolean } True if this path exists and is a file.
*/
function isFile( file ) {
if ( ! fs.existsSync( file ) ) {
return false;
}
const stats = fs.statSync( file );
return stats && stats.isFile();
}
/**
* Create a table of contents given a list of markdown files.
*
* @param { array } files A list of readme files.
*/
function writeTableOfContents( files ) {
const mdFiles = files.map( f => getMdFileName( f, false ) );
const TOC = uniq( mdFiles ).map( doc => {
const name = camelCaseDash( doc.replace( '.md', '' ) );
return ` * [${ name }](components/${ doc })`;
} ).join( '\n' );
const TocFile = path.resolve( DOCS_FOLDER, '../_sidebar.md' );
fs.writeFileSync( TocFile, '* Home\n\n * [Overview](/)\n\n* Components\n\n' + TOC );
}
module.exports = {
COMPONENTS_FOLDER,
DOCS_FOLDER,
deleteExistingDocs,
getExportedFileList,
getMdFileName,
getRealFilePaths,
writeTableOfContents,
};

View File

@ -0,0 +1,169 @@
/** @format */
/**
* External dependencies
*/
const { map } = require( 'lodash' );
/**
* Given a string, returns a new string with dash separators converted to
* camel-case equivalent. This is not as aggressive as `_.camelCase` in
* converting to uppercase, where Lodash will convert letters following
* numbers.
*
* @param {string} string Input dash-delimited string.
*
* @return {string} Camel-cased string.
*/
function camelCaseDash( string ) {
return string
.replace( /^([a-z])/g, ( match, letter ) => letter.toUpperCase() )
.replace( /-([a-z])/g, ( match, letter ) => letter.toUpperCase() );
}
/**
* Get a formatted description string.
*
* @param { string } description Component description as retrieved from component docs.
* @return { string } Formatted string.
*/
function getDescription( description = '' ) {
// eslint requires valid jsdoc, but we can remove this because it's implicit.
return description.replace( '@return { object } -', '' ) + '\n';
}
/**
* Get a single prop's details formatted for markdown.
*
* @param { string } propName Prop name.
* @param { object } prop Prop details as retrieved from component docs.
* @return { string } Formatted string.
*/
function getProp( propName, prop ) {
const lines = [ '### `' + propName + '`\n' ];
prop.required && lines.push( '- **Required**' );
lines.push( '- Type: ' + getPropType( prop.type, propName ) );
lines.push( '- Default: ' + getPropDefaultValue( prop.defaultValue ) );
lines.push( '\n' );
lines.push( prop.description );
lines.push( '\n' );
return lines.filter( Boolean ).join( '\n' );
}
/**
* Get a single prop's default value.
*
* @param { object } value Default value as retrieved from component docs.
* @return { string } Formatted string.
*/
function getPropDefaultValue( value ) {
if ( value && value.value ) {
return '`' + value.value + '`';
}
return 'null';
}
/**
* Get props and prop details formatted for markdown.
*
* @param { object } props Component props as retrieved from component docs.
* @return { string } Formatted string.
*/
function getProps( props = {} ) {
const title = 'Props';
const lines = [ title, stringOfLength( '-', title.length ), '' ];
Object.keys( props ).map( key => {
lines.push( getProp( key, props[ key ] ) );
} );
return lines.join( '\n' );
}
/**
* Get a single prop's type.
*
* @param { object } type Prop type as retrieved from component docs.
* @return { string } Formatted string.
*/
function getPropType( type ) {
if ( ! type ) {
return;
}
const labels = {
func: 'Function',
array: 'Array',
object: 'Object',
string: 'String',
number: 'Number',
bool: 'Boolean',
node: 'ReactNode',
element: 'ReactElement',
any: '*',
custom: '(custom validator)',
};
let value = '';
switch ( type.name ) {
case 'arrayOf':
// replacing "Object" is a hack for shape proptypes.
value = 'Array \n' + getPropType( type.value ).replace( 'Object \n', '' );
break;
case 'objectOf':
// replacing "Object" is a hack for shape proptypes.
value = 'Object \n' + getPropType( type.value ).replace( 'Object \n', '' );
break;
case 'shape':
value = map( type.value, ( v, key ) => `\n - ${ key }: ` + getPropType( v ) ).join( '' );
value = 'Object \n' + value.replace( /^\n/, '' );
break;
case 'enum':
value = 'One of: ' + type.value.map( v => v.value ).join( ', ' );
break;
case 'union':
value = 'One of type: ' + type.value.map( v => v.name ).join( ', ' );
break;
default:
value = labels[ type.name ] || type.name;
}
return value;
}
/**
* Get a formatted title string.
*
* @param { string } name Component title as retrieved from component docs.
* @return { string } Formatted string.
*/
function getTitle( name ) {
const title = '`' + name + '` (component)';
return title + '\n' + stringOfLength( '=', title.length ) + '\n';
}
/**
* Repeat a string n times. If the string is 1 character long,
* this will return a string of length n.
*
* @param { string } string String to repeat.
* @param { number } n Number to repeat the string.
* @return { string } New string.
*/
function stringOfLength( string, n ) {
let newString = '';
for ( let i = 0; i < n; i++ ) {
newString += string;
}
return newString;
}
module.exports = {
camelCaseDash,
getDescription,
getProp,
getPropDefaultValue,
getProps,
getPropType,
getTitle,
stringOfLength,
};

View File

@ -12,6 +12,10 @@ import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import './style.scss';
/**
* This component creates slideable content controlled by an animate prop to direct the contents to slide left or right.
* All other props are passed to `CSSTransition`. More info at http://reactcommunity.org/react-transition-group/css-transition
*/
class AnimationSlider extends Component {
constructor() {
super();
@ -62,9 +66,21 @@ class AnimationSlider extends Component {
}
AnimationSlider.propTypes = {
/**
* A function returning rendered content with argument status, reflecting `CSSTransition` status.
*/
children: PropTypes.func.isRequired,
/**
* A unique identifier for each slideable page.
*/
animationKey: PropTypes.any.isRequired,
/**
* null, 'left', 'right', to designate which direction to slide on a change.
*/
animate: PropTypes.oneOf( [ null, 'left', 'right' ] ),
/**
* When set to true, the first focusable element will be focused after an animation has finished.
*/
focusOnChange: PropTypes.bool,
};

View File

@ -24,6 +24,9 @@ import phrases from './phrases';
import { validateDateInputForRange } from 'lib/date';
import './style.scss';
/**
* This is wrapper for a [react-dates](https://github.com/airbnb/react-dates) powered calendar.
*/
class DateRange extends Component {
constructor( props ) {
super( props );
@ -170,18 +173,49 @@ class DateRange extends Component {
}
DateRange.propTypes = {
/**
* A moment date object representing the selected start. `null` for no selection.
*/
after: PropTypes.object,
/**
* A string error message, shown to the user.
*/
afterError: PropTypes.string,
/**
* The start date in human-readable format. Displayed in the text input.
*/
afterText: PropTypes.string,
/**
* A moment date object representing the selected end. `null` for no selection.
*/
before: PropTypes.object,
onUpdate: PropTypes.func.isRequired,
/**
* A string error message, shown to the user.
*/
beforeError: PropTypes.string,
/**
* The end date in human-readable format. Displayed in the text input.
*/
beforeText: PropTypes.string,
/**
* String identifying which is the currently focused input (start or end).
*/
focusedInput: PropTypes.string,
/**
* Optionally invalidate certain days. `past`, `future`, `none`, or function are accepted.
* A function will be passed to react-dates' `isOutsideRange` prop
*/
invalidDays: PropTypes.oneOfType( [
PropTypes.oneOf( [ 'past', 'future', 'none' ] ),
PropTypes.func,
] ),
focusedInput: PropTypes.string,
afterText: PropTypes.string,
beforeText: PropTypes.string,
afterError: PropTypes.string,
beforeError: PropTypes.string,
/**
* A function called upon selection of a date.
*/
onUpdate: PropTypes.func.isRequired,
/**
* The date format in moment.js-style tokens.
*/
shortDateFormat: PropTypes.string.isRequired,
};

View File

@ -14,6 +14,9 @@ import EllipsisMenu from 'components/ellipsis-menu';
import { H, Section } from 'layout/section';
import { validateComponent } from 'lib/proptype-validator';
/**
* A basic card component with a header. The header can contain a title, an action, and an `EllipsisMenu` menu.
*/
class Card extends Component {
render() {
const { action, children, menu, title } = this.props;
@ -35,9 +38,21 @@ class Card extends Component {
}
Card.propTypes = {
/**
* One "primary" action for this card, appears in the card header.
*/
action: PropTypes.node,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* An `EllipsisMenu`, with filters used to control the content visible in this card
*/
menu: validateComponent( EllipsisMenu ),
/**
* The title to use for this card.
*/
title: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ).isRequired,
};

View File

@ -34,10 +34,15 @@ import {
getYTickOffset,
} from './utils';
/**
* A simple D3 line and bar chart component for timeseries data in React.
*/
class D3Chart extends Component {
constructor( props ) {
super( props );
this.drawChart = this.drawChart.bind( this );
this.getAllData = this.getAllData.bind( this );
this.getParams = this.getParams.bind( this );
this.state = {
allData: this.getAllData( props ),
width: props.width,
@ -64,7 +69,7 @@ class D3Chart extends Component {
return [ ...props.data, ...orderedKeys ];
}
drawChart = ( node, params ) => {
drawChart( node, params ) {
const { data, margin, type } = this.props;
const g = node
.attr( 'id', 'chart' )
@ -81,9 +86,9 @@ class D3Chart extends Component {
type === 'bar' && drawBars( g, data, adjParams );
return node;
};
}
getParams = node => {
getParams( node ) {
const { colorScheme, data, height, margin, orderedKeys, type, xFormat, yFormat } = this.props;
const { width } = this.state;
const calculatedWidth = width || node.offsetWidth;
@ -121,7 +126,7 @@ class D3Chart extends Component {
yTickOffset: getYTickOffset( adjHeight, scale, yMax ),
yFormat: d3Format( yFormat ),
};
};
}
render() {
if ( ! this.props.data ) {
@ -146,21 +151,54 @@ class D3Chart extends Component {
}
D3Chart.propTypes = {
colorScheme: PropTypes.func,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* A chromatic color function to be passed down to d3.
*/
colorScheme: PropTypes.func,
/**
* An array of data.
*/
data: PropTypes.array.isRequired,
/**
* Relative viewpoirt height of the `svg`.
*/
height: PropTypes.number,
/**
* @todo Remove not used?
*/
legend: PropTypes.array,
/**
* Margins for axis and chart padding.
*/
margin: PropTypes.shape( {
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
} ),
/**
* The list of labels for this chart.
*/
orderedKeys: PropTypes.array,
/**
* Chart type of either `line` or `bar`.
*/
type: PropTypes.oneOf( [ 'bar', 'line' ] ),
/**
* Relative viewport width of the `svg`.
*/
width: PropTypes.number,
/**
* A datetime formatting string, passed to d3TimeFormat.
*/
xFormat: PropTypes.string,
/**
* A number formatting string, passed to d3Format.
*/
yFormat: PropTypes.string,
};

View File

@ -35,6 +35,9 @@ function getOrderedKeys( data ) {
.sort( ( a, b ) => b.total - a.total );
}
/**
* A chart container using d3, to display timeseries data with an interactive legend.
*/
class Chart extends Component {
constructor( props ) {
super( props );
@ -166,7 +169,13 @@ class Chart extends Component {
}
Chart.propTypes = {
/**
* An array of data.
*/
data: PropTypes.array.isRequired,
/**
* A title describing this chart.
*/
title: PropTypes.string,
};

View File

@ -13,6 +13,9 @@ import './style.scss';
import { formatCurrency } from 'lib/currency';
import { getColor } from './utils';
/**
* A legend specifically designed for the WooCommerce admin charts.
*/
class Legend extends Component {
render() {
const {
@ -69,11 +72,29 @@ class Legend extends Component {
}
Legend.propTypes = {
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* A chromatic color function to be passed down to d3.
*/
colorScheme: PropTypes.func,
/**
* An array of `orderedKeys`.
*/
data: PropTypes.array.isRequired,
/**
* Handles `onClick` event.
*/
handleLegendToggle: PropTypes.func,
/**
* Handles `onMouseEnter`/`onMouseLeave` events.
*/
handleLegendHover: PropTypes.func,
/**
* Display legend items as a `row` or `column` inside a flex-box.
*/
legendDirection: PropTypes.oneOf( [ 'row', 'column' ] ),
};

View File

@ -9,6 +9,11 @@ import PropTypes from 'prop-types';
*/
import './style.scss';
/**
* Display a number with a styled border.
*
* @return { object } -
*/
const Count = ( { count, label } ) => {
if ( ! label ) {
label = sprintf( __( 'Total %d', 'wc-admin' ), count );
@ -22,7 +27,13 @@ const Count = ( { count, label } ) => {
};
Count.propTypes = {
/**
* Value of the number to be displayed.
*/
count: PropTypes.number.isRequired,
/**
* A translated label with the number in context, used for screen readers.
*/
label: PropTypes.string,
};

View File

@ -11,6 +11,13 @@ import classnames from 'classnames';
*/
import './style.scss';
/**
* A button useful for a launcher of a dropdown component. The button is 100% width of its container and displays
* single or multiple lines rendered as `<span/>` elments.
*
* @param { object } props Props passed to component.
* @return { object } -
*/
const DropdownButton = props => {
const { labels, isOpen, ...otherProps } = props;
const buttonClasses = classnames( 'woocommerce-dropdown-button', {
@ -27,7 +34,13 @@ const DropdownButton = props => {
};
DropdownButton.propTypes = {
/**
* An array of elements to be rendered as the content of the button.
*/
labels: PropTypes.array,
/**
* Boolean describing if the dropdown in open or not.
*/
isOpen: PropTypes.bool,
};

View File

@ -12,6 +12,9 @@ import PropTypes from 'prop-types';
*/
import './style.scss';
/**
* This is a dropdown menu hidden behind a vertical ellipsis icon. When clicked, the inner MenuItems are displayed.
*/
class EllipsisMenu extends Component {
render() {
const { children, label } = this.props;
@ -53,7 +56,14 @@ class EllipsisMenu extends Component {
}
EllipsisMenu.propTypes = {
/**
* The label shown when hovering/focusing on the icon button.
*/
label: PropTypes.string.isRequired,
/**
* A list of `MenuTitle`/`MenuItem` components
*/
children: PropTypes.node,
};
export default EllipsisMenu;

View File

@ -6,6 +6,11 @@ import { Component } from '@wordpress/element';
import { ENTER, SPACE } from '@wordpress/keycodes';
import PropTypes from 'prop-types';
/**
* `MenuItem` is used to give the item an accessible wrapper, with the `menuitem` role and added keyboard functionality (`onInvoke`).
* `MenuItem`s can also be deemed "clickable", though this is disabled by default because generally the inner component handles
* the click event.
*/
class MenuItem extends Component {
constructor() {
super( ...arguments );
@ -46,8 +51,19 @@ class MenuItem extends Component {
}
MenuItem.propTypes = {
/**
* A renderable component (or string) which will be displayed as the content of this item. Generally a `ToggleControl`.
*/
children: PropTypes.node,
/**
* Boolean to control whether the MenuItem should handle the click event. Defaults to false, assuming your child component
* handles the click event.
*/
isClickable: PropTypes.bool,
/**
* A function called when this item is activated via keyboard ENTER or SPACE; or when the item is clicked
* (only if `isClickable` is set).
*/
onInvoke: PropTypes.func.isRequired,
};

View File

@ -4,11 +4,20 @@
*/
import PropTypes from 'prop-types';
/**
* `MenuTitle` is another valid Menu child, but this does not have any accessibility attributes associated
* (so this should not be used in place of the `EllipsisMenu` prop `label`).
*
* @return { object } -
*/
const MenuTitle = ( { children } ) => {
return <div className="woocommerce-ellipsis-menu__title">{ children }</div>;
};
MenuTitle.propTypes = {
/**
* A renderable component (or string) which will be displayed as the content of this item.
*/
children: PropTypes.node,
};

View File

@ -20,6 +20,9 @@ const matches = [
{ value: 'any', label: __( 'Any', 'wc-admin' ) },
];
/**
* Displays a configurable set of filters which can modify query parameters.
*/
class AdvancedFilters extends Component {
constructor( props ) {
super( props );
@ -231,9 +234,28 @@ class AdvancedFilters extends Component {
}
AdvancedFilters.propTypes = {
config: PropTypes.object.isRequired,
/**
* The configuration object required to render filters.
*/
config: PropTypes.objectOf(
PropTypes.shape( {
label: PropTypes.string,
addLabel: PropTypes.string,
rules: PropTypes.arrayOf( PropTypes.object ),
input: PropTypes.object,
} )
).isRequired,
/**
* Name of this filter, used in translations.
*/
filterTitle: PropTypes.string.isRequired,
/**
* The `path` parameter supplied by React-Router.
*/
path: PropTypes.string.isRequired,
/**
* The query string represented in object form.
*/
query: PropTypes.object,
};

View File

@ -18,6 +18,9 @@ import './style.scss';
const shortDateFormat = __( 'MM/DD/YYYY', 'wc-admin' );
/**
* Select a range of dates or single dates.
*/
class DatePicker extends Component {
constructor( props ) {
super( props );
@ -148,7 +151,13 @@ class DatePicker extends Component {
}
DatePicker.propTypes = {
/**
* The `path` parameter supplied by React-Router.
*/
path: PropTypes.string.isRequired,
/**
* The query string represented in object form.
*/
query: PropTypes.object,
};

View File

@ -19,6 +19,10 @@ import './style.scss';
export const DEFAULT_FILTER = 'all';
/**
* Modify a url query parameter via a dropdown selection of configurable options.
* This component manipulates the `filter` query parameter.
*/
class FilterPicker extends Component {
constructor( props ) {
super( props );
@ -176,8 +180,41 @@ class FilterPicker extends Component {
}
FilterPicker.propTypes = {
filters: PropTypes.array.isRequired,
/**
* An array of filters and subFilters to construct the menu.
*/
filters: PropTypes.arrayOf(
PropTypes.shape( {
/**
* A custom component used instead of a button, might have special handling for filtering. TBD, not yet implemented.
*/
component: PropTypes.string,
/**
* The label for this filter. Optional only for custom component filters.
*/
label: PropTypes.string,
/**
* An array representing the "path" to this filter, if nested.
*/
path: PropTypes.string,
/**
* An array of more filter objects that act as "children" to this item.
* This set of filters is shown if the parent filter is clicked.
*/
subFilters: PropTypes.array,
/**
* The value for this filter, used to set the `filter` query param when clicked, if there are no `subFilters`.
*/
value: PropTypes.string.isRequired,
} )
).isRequired,
/**
* The `path` parameter supplied by React-Router.
*/
path: PropTypes.string.isRequired,
/**
* The query string represented in object form.
*/
query: PropTypes.object,
};

View File

@ -18,6 +18,12 @@ import FilterPicker from './filter';
import { H, Section } from 'layout/section';
import './style.scss';
/**
* Add a collection of report filters to a page. This uses `DatePicker` & `FilterPicker` for the "basic" filters, and `AdvancedFilters`
* or a comparison card if "advanced" or "compare" are picked from `FilterPicker`.
*
* @return { object } -
*/
const ReportFilters = ( { advancedConfig, filters, query, path } ) => {
let advancedCard = false;
switch ( query.filter ) {
@ -70,9 +76,21 @@ const ReportFilters = ( { advancedConfig, filters, query, path } ) => {
};
ReportFilters.propTypes = {
/**
* Config option passed through to `AdvancedFilters`
*/
advancedConfig: PropTypes.object,
/**
* Config option passed through to `FilterPicker` - if not used, `FilterPicker` is not displayed.
*/
filters: PropTypes.array,
/**
* The `path` parameter supplied by React-Router
*/
path: PropTypes.string.isRequired,
/**
* The query string represented in object form
*/
query: PropTypes.object,
};

View File

@ -12,6 +12,11 @@ import PropTypes from 'prop-types';
*/
import './style.scss';
/**
* Use the `Flag` component to display a country's flag.
*
* @return { object } -
*/
const Flag = ( { code, order, round, height, width, className } ) => {
const classes = classnames( 'woocommerce-flag', className, {
'is-round': round,
@ -42,11 +47,29 @@ const Flag = ( { code, order, round, height, width, className } ) => {
};
Flag.propTypes = {
/**
* Two letter, three letter or three digit country code.
*/
code: PropTypes.string,
/**
* An order can be passed instead of `code` and the code will automatically be pulled from the billing or shipping data.
*/
order: PropTypes.object,
/**
* True to display a rounded flag.
*/
round: PropTypes.bool,
/**
* Flag image height.
*/
height: PropTypes.number,
/**
* Flag image width.
*/
width: PropTypes.number,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
};

View File

@ -24,5 +24,4 @@ render: function() {
* `size`: Default 60. The size of Gravatar to request.
* `alt`: Text to display as the image alt attribute.
* `title`: Text to use for the image's title
* `fallback`: Default `mp`. Gravatar default fallback mode. See https://en.gravatar.com/site/implement/images/.
* `className`: Additional CSS classes.

View File

@ -14,6 +14,11 @@ import crypto from 'crypto';
*/
import './style.scss';
/**
* Display a users Gravatar.
*
* @return { object } -
*/
const Gravatar = ( { alt, title, size, user, className } ) => {
const classes = classnames( 'woocommerce-gravatar', className, {
'is-placeholder': ! user,
@ -62,10 +67,25 @@ const Gravatar = ( { alt, title, size, user, className } ) => {
};
Gravatar.propTypes = {
/**
* The address to hash for displaying a Gravatar. Can be an email address or WP-API user object.
*/
user: PropTypes.oneOfType( [ PropTypes.object, PropTypes.string ] ),
/**
* Text to display as the image alt attribute.
*/
alt: PropTypes.string,
/**
* Text to use for the image's title
*/
title: PropTypes.string,
/**
* Default 60. The size of Gravatar to request.
*/
size: PropTypes.number,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
};

View File

@ -35,8 +35,8 @@ export { default as SegmentedSelection } from './segmented-selection';
export { default as SplitButton } from './split-button';
export { default as SummaryList } from './summary';
export { default as SummaryNumber } from './summary/item';
export { default as TableCard } from './table';
export { default as Table } from './table/table';
export { default as TableCard } from './table';
export { default as TablePlaceholder } from './table/placeholder';
export { default as TableSummary } from './table/summary';
export { default as Tag } from './tag';

View File

@ -11,6 +11,10 @@ import { Link as RouterLink } from 'react-router-dom';
*/
import { getAdminLink } from 'lib/nav-utils';
/**
* Use `Link` to create a link to another resource. It accepts a type to automatically
* create wp-admin links, wc-admin links, and external links.
*/
class Link extends Component {
render() {
const { children, href, type, ...props } = this.props;
@ -40,7 +44,13 @@ class Link extends Component {
}
Link.propTypes = {
/**
* The resource to link to.
*/
href: PropTypes.string.isRequired,
/**
* Type of link. For wp-admin and wc-admin, the correct prefix is appended.
*/
type: PropTypes.oneOf( [ 'wp-admin', 'wc-admin', 'external' ] ).isRequired,
};

View File

@ -11,6 +11,11 @@ import PropTypes from 'prop-types';
*/
import './style.scss';
/**
* Use `OrderStatus` to display a badge with human-friendly text describing the current order status.
*
* @return { object } -
*/
const OrderStatus = ( { order, className } ) => {
const { status } = order;
const { orderStatuses } = wcSettings;
@ -28,7 +33,13 @@ const OrderStatus = ( { order, className } ) => {
};
OrderStatus.propTypes = {
/**
* The order to display a status for.
*/
order: PropTypes.object.isRequired,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
};

View File

@ -15,6 +15,10 @@ import { isFinite, noop, uniqueId } from 'lodash';
*/
import './style.scss';
/**
* Use `Pagination` to allow navigation between pages that represent a collection of items.
* The component allows for selecting a new page and items per page options.
*/
class Pagination extends Component {
constructor( props ) {
super( props );
@ -182,11 +186,29 @@ class Pagination extends Component {
}
Pagination.propTypes = {
/**
* The current page of the collection.
*/
page: PropTypes.number.isRequired,
/**
* A function to execute when the page is changed.
*/
onPageChange: PropTypes.func,
/**
* The amount of results that are being displayed per page.
*/
perPage: PropTypes.number.isRequired,
/**
* A function to execute when the per page option is changed.
*/
onPerPageChange: PropTypes.func,
/**
* The total number of results.
*/
total: PropTypes.number.isRequired,
/**
* Additional classNames.
*/
className: PropTypes.string,
};

View File

@ -11,6 +11,12 @@ import PropTypes from 'prop-types';
*/
import './style.scss';
/**
* Use `ProductImage` to display a product's featured image. If no image can be found, a placeholder matching the front-end image
* placeholder will be displayed.
*
* @return { object } -
*/
const ProductImage = ( { product, alt, width, height, className, ...props } ) => {
// The first returned image from the API is the featured/product image.
const productImage = product && product.images && product.images[ 0 ];
@ -34,10 +40,26 @@ const ProductImage = ( { product, alt, width, height, className, ...props } ) =>
};
ProductImage.propTypes = {
/**
* The width of image to display.
*/
width: PropTypes.number,
/**
* The height of image to display.
*/
height: PropTypes.number,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* Product object. The image to display will be pulled from `product.images`.
* See https://woocommerce.github.io/woocommerce-rest-api-docs/#product-properties
*/
product: PropTypes.object,
/**
* Text to use as the image alt attribute.
*/
alt: PropTypes.string,
};

View File

@ -14,6 +14,10 @@ import PropTypes from 'prop-types';
*/
import './style.scss';
/**
* Use `Rating` to display a set of stars, filled, empty or half-filled, that represents a
* rating in a scale between 0 and the prop `totalStars` (default 5).
*/
class Rating extends Component {
stars() {
const { size, totalStars } = this.props;
@ -52,9 +56,21 @@ class Rating extends Component {
}
Rating.propTypes = {
/**
* Number of stars that should be filled. You can pass a partial number of stars like `2.5`.
*/
rating: PropTypes.number,
/**
* The total number of stars the rating is out of.
*/
totalStars: PropTypes.number,
/**
* The size in pixels the stars should be rendered at.
*/
size: PropTypes.number,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
};

View File

@ -10,12 +10,21 @@ import PropTypes from 'prop-types';
*/
import Rating from './index';
/**
* Display a set of stars representing the product's average rating.
*
* @return { object } -
*/
const ProductRating = ( { product, ...props } ) => {
const rating = ( product && product.average_rating ) || 0;
return <Rating rating={ rating } { ...props } />;
};
ProductRating.propTypes = {
/**
* A product object containing a `average_rating`.
* See https://woocommerce.github.io/woocommerce-rest-api-docs/#products.
*/
product: PropTypes.object.isRequired,
};

View File

@ -10,12 +10,21 @@ import PropTypes from 'prop-types';
*/
import Rating from './index';
/**
* Display a set of stars representing the review's rating.
*
* @return { object } -
*/
const ReviewRating = ( { review, ...props } ) => {
const rating = ( review && review.rating ) || 0;
return <Rating rating={ rating } { ...props } />;
};
ReviewRating.propTypes = {
/**
* A review object containing a `rating`.
* See https://woocommerce.github.io/woocommerce-rest-api-docs/#retrieve-product-reviews.
*/
review: PropTypes.object.isRequired,
};

View File

@ -15,6 +15,10 @@ import { product } from './autocompleters';
import Tag from 'components/tag';
import './style.scss';
/**
* A search box which autocompletes results while typing, allowing for the user to select an existing object
* (product, order, customer, etc). Currently only products are supported.
*/
class Search extends Component {
constructor( props ) {
super( props );
@ -116,7 +120,13 @@ class Search extends Component {
}
Search.propTypes = {
/**
* Function called when selected results change, passed result list.
*/
onChange: PropTypes.func,
/**
* The object type to be used in searching.
*/
type: PropTypes.oneOf( [ 'products', 'orders', 'customers' ] ).isRequired,
};

View File

@ -12,6 +12,9 @@ import { partial, uniqueId } from 'lodash';
*/
import './style.scss';
/**
* Create a panel of styled selectable options rendering stylized checkboxes and labels
*/
class SegmentedSelection extends Component {
render() {
const { className, options, selected, onSelect, name, legend } = this.props;
@ -49,16 +52,34 @@ class SegmentedSelection extends Component {
}
SegmentedSelection.propTypes = {
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* An Array of options to render. The array needs to be composed of objects with properties `label` and `value`.
*/
options: PropTypes.arrayOf(
PropTypes.shape( {
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
} )
).isRequired,
/**
* Value of selected item.
*/
selected: PropTypes.string,
/**
* Callback to be executed after selection
*/
onSelect: PropTypes.func.isRequired,
/**
* This will be the key in the key and value arguments supplied to `onSelect`.
*/
name: PropTypes.string.isRequired,
/**
* Create a legend visible to screen readers.
*/
legend: PropTypes.string.isRequired,
};

View File

@ -13,6 +13,11 @@ import { noop } from 'lodash';
*/
import './style.scss';
/**
* A component for displaying a button with a main action plus a secondary set of actions behind a menu toggle.
*
* @return { object } -
*/
const SplitButton = ( {
isPrimary,
mainIcon,
@ -100,18 +105,48 @@ const SplitButton = ( {
};
SplitButton.propTypes = {
/**
* Whether the button is styled as a primary button.
*/
isPrimary: PropTypes.bool,
/**
* Icon for the main button.
*/
mainIcon: PropTypes.node,
/**
* Label for the main button.
*/
mainLabel: PropTypes.string,
/**
* Function to activate when the the main button is clicked.
*/
onClick: PropTypes.func,
/**
* Label to display for the menu of actions, used as a heading on the mobile popover and for accessible text.
*/
menuLabel: PropTypes.string,
/**
* An array of additional actions. Accepts additional icon, label, and onClick props.
*/
controls: PropTypes.arrayOf(
PropTypes.shape( {
icon: PropTypes.node,
label: PropTypes.string,
/**
* Icon used in button, passed to `IconButton`. Can be either string (dashicon name) or Gridicon.
*/
icon: PropTypes.oneOfType( [ PropTypes.string, PropTypes.element ] ),
/**
* Label displayed for this button.
*/
label: PropTypes.string.isRequired,
/**
* Click handler for this button.
*/
onClick: PropTypes.func,
} )
).isRequired,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
};

View File

@ -15,6 +15,12 @@ import { uniqueId } from 'lodash';
import { isMobileViewport, isTabletViewport } from 'lib/ui';
import './style.scss';
/**
* A container element for a list of SummaryNumbers. This component handles detecting & switching to
* the mobile format on smaller screens.
*
* @return { object } -
*/
const SummaryList = ( { children, label } ) => {
if ( ! label ) {
label = __( 'Performance Indicators', 'wc-admin' );
@ -70,7 +76,13 @@ const SummaryList = ( { children, label } ) => {
};
SummaryList.propTypes = {
/**
* A list of `<SummaryNumber />`s
*/
children: PropTypes.node.isRequired,
/**
* An optional label of this group, read to screen reader users. Defaults to "Performance Indicators".
*/
label: PropTypes.string,
};

View File

@ -14,6 +14,11 @@ import PropTypes from 'prop-types';
*/
import Link from 'components/link';
/**
* A component to show a value, label, and an optional change percentage. Can also act as a link to a specific report focus.
*
* @return { object } -
*/
const SummaryNumber = ( {
delta,
href,
@ -95,15 +100,49 @@ const SummaryNumber = ( {
};
SummaryNumber.propTypes = {
/**
* A number to represent the percentage change since the last comparison period - positive numbers will show
* a green up arrow, negative numbers will show a red down arrow, and zero will show a flat right arrow.
* If omitted, no change value will display.
*/
delta: PropTypes.number,
/**
* An internal link to the report focused on this number.
*/
href: PropTypes.string.isRequired,
/**
* Boolean describing whether the menu list is open. Only applies in mobile view,
* and only applies to the toggle-able item (first in the list).
*/
isOpen: PropTypes.bool,
/**
* A string description of this value, ex "Revenue", or "New Customers"
*/
label: PropTypes.string.isRequired,
/**
* A function used to switch the given SummaryNumber to a button, and called on click.
*/
onToggle: PropTypes.func,
/**
* A string description of the previous value's timeframe, ex "Previous Year:".
*/
prevLabel: PropTypes.string,
/**
* A string or number value to display - a string is allowed so we can accept currency formatting.
* If omitted, this section won't display.
*/
prevValue: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ),
/**
* A boolean used to indicate that a negative delta is "good", and should be styled like a positive (and vice-versa).
*/
reverseTrend: PropTypes.bool,
/**
* A boolean used to show a highlight style on this number.
*/
selected: PropTypes.bool,
/**
* A string or number value to display - a string is allowed so we can accept currency formatting.
*/
value: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
};

View File

@ -20,6 +20,14 @@ import Pagination from 'components/pagination';
import Table from './table';
import TableSummary from './summary';
/**
* This is an accessible, sortable, and scrollable table for displaying tabular data (like revenue and other analytics data).
* It accepts `headers` for column headers, and `rows` for the table content.
* `rowHeader` can be used to define the index of the row header (or false if no header).
*
* `TableCard` serves as Card wrapper & contains a card header, `<Table />`, `<TableSummary />`, and `<Pagination />`.
* This includes filtering and comparison functionality for report pages.
*/
class TableCard extends Component {
constructor( props ) {
super( props );
@ -122,6 +130,9 @@ class TableCard extends Component {
}
TableCard.propTypes = {
/**
* An array of column headers (see `Table` props).
*/
headers: PropTypes.arrayOf(
PropTypes.shape( {
defaultSort: PropTypes.bool,
@ -131,10 +142,25 @@ TableCard.propTypes = {
required: PropTypes.bool,
} )
),
/**
* A function which returns a callback function to update the query string for a given `param`.
*/
onQueryChange: PropTypes.func,
/**
* A callback function which handles then "download" button press. Optional, if not used, the button won't appear.
*/
onClickDownload: PropTypes.func,
/**
* An object of the query parameters passed to the page, ex `{ page: 2, per_page: 5 }`.
*/
query: PropTypes.object,
/**
* An array of arrays of display/value object pairs (see `Table` props).
*/
rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ),
/**
* Which column should be the row header, defaults to the first item (`0`) (see `Table` props).
*/
rows: PropTypes.arrayOf(
PropTypes.arrayOf(
PropTypes.shape( {
@ -143,12 +169,19 @@ TableCard.propTypes = {
} )
)
).isRequired,
/**
* An array of objects with `label` & `value` properties, which display in a line under the table.
* Optional, can be left off to show no summary.
*/
summary: PropTypes.arrayOf(
PropTypes.shape( {
label: PropTypes.node,
value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] ),
} )
),
/**
* The title used in the card header, also used as the caption for the content in this table.
*/
title: PropTypes.string.isRequired,
};

View File

@ -4,6 +4,11 @@
*/
import PropTypes from 'prop-types';
/**
* A component to display summarized table data - the list of data passed in on a single line.
*
* @return { object } -
*/
const TableSummary = ( { data } ) => {
return (
<ul className="woocommerce-table__summary">
@ -18,6 +23,9 @@ const TableSummary = ( { data } ) => {
};
TableSummary.propTypes = {
/**
* An array of objects with `label` & `value` properties, which display on a single line.
*/
data: PropTypes.array,
};

View File

@ -15,6 +15,9 @@ const DESC = 'desc';
const getDisplay = cell => cell.display || null;
/**
* A table component, without the Card wrapper. This is a basic table display, sortable, but no default filtering.
*/
class Table extends Component {
constructor( props ) {
super( props );
@ -149,28 +152,79 @@ class Table extends Component {
}
Table.propTypes = {
/**
* Controls whether this component is hidden from screen readers. Used by the loading state, before there is data to read.
* Don't use this on real tables unless the table data is loaded elsewhere on the page.
*/
ariaHidden: PropTypes.bool,
/**
* A label for the content in this table
*/
caption: PropTypes.string.isRequired,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* An array of column headers, as objects.
*/
headers: PropTypes.arrayOf(
PropTypes.shape( {
/**
* Boolean, true if this column is the default for sorting. Only one column should have this set.
*/
defaultSort: PropTypes.bool,
/**
* Boolean, true if this column is a number value.
*/
isNumeric: PropTypes.bool,
/**
* Boolean, true if this column is sortable.
*/
isSortable: PropTypes.bool,
/**
* The API parameter name for this column, passed to `orderby` when sorting via API.
*/
key: PropTypes.string,
/**
* The display label for this column.
*/
label: PropTypes.string,
/**
* Boolean, true if this column should always display in the table (not shown in toggle-able list).
*/
required: PropTypes.bool,
} )
),
/**
* A function called when sortable table headers are clicked, gets the `header.key` as argument.
*/
onSort: PropTypes.func,
/**
* The query string represented in object form
*/
query: PropTypes.object,
/**
* An array of arrays of display/value object pairs.
*/
rows: PropTypes.arrayOf(
PropTypes.arrayOf(
PropTypes.shape( {
/**
* Display value, used for rendering- strings or elements are best here.
*/
display: PropTypes.node,
/**
* "Real" value used for sorting, and should be a string or number. A column with `false` value will not be sortable.
*/
value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number, PropTypes.bool ] ),
} )
)
).isRequired,
/**
* Which column should be the row header, defaults to the first item (`0`) (but could be set to `1`, if the first col
* is checkboxes, for example). Set to false to disable row headers.
*/
rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ),
};

View File

@ -14,6 +14,12 @@ import { withInstanceId } from '@wordpress/compose';
*/
import './style.scss';
/**
* This component can be used to show an item styled as a "tag", optionally with an `X` + "remove".
* Generally this is used in a collection of selected items, see the Search component.
*
* @return { object } -
*/
const Tag = ( { id, instanceId, label, remove, removeLabel, screenReaderLabel, className } ) => {
screenReaderLabel = screenReaderLabel || label;
const classes = classnames( 'woocommerce-tag', className, {
@ -41,10 +47,25 @@ const Tag = ( { id, instanceId, label, remove, removeLabel, screenReaderLabel, c
};
Tag.propTypes = {
/**
* The ID for this item, used in the remove function.
*/
id: PropTypes.number.isRequired,
/**
* The name for this item, displayed as the tag's text.
*/
label: PropTypes.string.isRequired,
/**
* A function called when the remove X is clicked. If not used, no X icon will display.
*/
remove: PropTypes.func,
/**
* The label for removing this item (shown when hovering on X, or read to screen reader users). Defaults to "Remove tag".
*/
removeLabel: PropTypes.string,
/**
* A more descriptive label for screen reader users. Defaults to the `name` prop.
*/
screenReaderLabel: PropTypes.string,
};

View File

@ -15570,6 +15570,69 @@
"react-with-styles-interface-css": "^4.0.1"
}
},
"react-docgen": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-2.21.0.tgz",
"integrity": "sha512-8xNPTrmvHLGNfqlsCYPdXmSkagP1njI5unP3t8WrjTJ4/5hHuP5nb3XH69CnF67HPV5zTkPoafcRBDGSQO6S6A==",
"dev": true,
"requires": {
"async": "^2.1.4",
"babel-runtime": "^6.9.2",
"babylon": "~5.8.3",
"commander": "^2.9.0",
"doctrine": "^2.0.0",
"node-dir": "^0.1.10",
"recast": "^0.12.6"
},
"dependencies": {
"ast-types": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.10.1.tgz",
"integrity": "sha512-UY7+9DPzlJ9VM8eY0b2TUZcZvF+1pO0hzMtAyjBYKhOmnvRlqYNYnWdtsMj0V16CGaMlpL0G1jnLbLo4AyotuQ==",
"dev": true
},
"babylon": {
"version": "5.8.38",
"resolved": "https://registry.npmjs.org/babylon/-/babylon-5.8.38.tgz",
"integrity": "sha1-7JsSCxG/bM1Bc6GL8hfmC3mFn/0=",
"dev": true
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
},
"node-dir": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
"integrity": "sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU=",
"dev": true,
"requires": {
"minimatch": "^3.0.2"
}
},
"recast": {
"version": "0.12.9",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.12.9.tgz",
"integrity": "sha512-y7ANxCWmMW8xLOaiopiRDlyjQ9ajKRENBH+2wjntIbk3A6ZR1+BLQttkmSHMY7Arl+AAZFwJ10grg2T6f1WI8A==",
"dev": true,
"requires": {
"ast-types": "0.10.1",
"core-js": "^2.4.1",
"esprima": "~4.0.0",
"private": "~0.1.5",
"source-map": "~0.6.1"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"react-dom": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.1.tgz",
@ -15873,9 +15936,9 @@
}
},
"recast": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.15.2.tgz",
"integrity": "sha512-L4f/GqxjlEJ5IZ+tdll/l+6dVi2ylysWbkgFJbMuldD6Jklgfv6zJnCpuAZDfjwHhfcd/De0dDKelsTEPQ29qA==",
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.15.3.tgz",
"integrity": "sha512-xqnagxQH7mL4+UpcCVMObPPdjCEE2dmfGcTwcdpyNgZOd9W0rfdLRF3+smoA+AQqMw6xK6G4021dAQK8XfPYIQ==",
"dev": true,
"requires": {
"ast-types": "0.11.5",

View File

@ -44,6 +44,7 @@
"@wordpress/keycodes": "^1.0.1",
"@wordpress/postcss-themes": "^1.0.1",
"@wordpress/scripts": "^2.0.2",
"ast-types": "^0.11.5",
"autoprefixer": "9.0.1",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^8.2.6",
@ -74,10 +75,12 @@
"raw-loader": "^0.5.1",
"react": "^16.3.2",
"react-click-outside": "2.3.1",
"react-docgen": "^2.21.0",
"react-dom": "^16.3.2",
"react-router-dom": "^4.2.2",
"react-world-flags": "^1.2.4",
"readline-sync": "^1.4.9",
"recast": "^0.15.3",
"sass-loader": "^7.0.1",
"style-loader": "^0.21.0",
"webpack": "^4.8.3",