Add Timeline component

The overall component is split into smaller components:
  * Timeline item: an individual item (event) for the timeline.
  * Timeline group: a group of items grouped according to props.

See README.md for prop details.

Ordering and grouping is decided via props. Component also
exports helper objects to make it easier to pass in the correct
props for ordering and grouping.

Item titles and bodies can be passed in as either strings or
React elements. Icons must be React elements.

Has a storybook scenario along with unit and snapshot tests.


Co-authored-by: Valerie K <valerie@automattic.com>
Co-authored-by: Allen Snook <allen@allendav.com>
Co-authored-by: Kristófer R <kristofer.thorlaksson@automattic.com>
Co-authored-by: David Levin <davidlevin@Davids-MacBook-Pro-2.local>
This commit is contained in:
Valerie K 2020-06-17 04:44:08 +09:00 committed by GitHub
parent 21d65f4bc8
commit fe865b0c84
13 changed files with 1149 additions and 0 deletions

View File

@ -1,4 +1,5 @@
# 5.0.0 (Unreleased) # 5.0.0 (Unreleased)
- Added `<Timeline />` component.
- Added `<ImageUpload />` component. - Added `<ImageUpload />` component.
- Style form components for WordPress 5.3. - Style form components for WordPress 5.3.
- Fix CompareFilter options format (key prop vs. id). - Fix CompareFilter options format (key prop vs. id).

View File

@ -57,6 +57,7 @@ export { default as TableSummary } from './table/summary';
export { default as Tag } from './tag'; export { default as Tag } from './tag';
export { default as TextControl } from './text-control'; export { default as TextControl } from './text-control';
export { default as TextControlWithAffixes } from './text-control-with-affixes'; export { default as TextControlWithAffixes } from './text-control-with-affixes';
export { default as Timeline } from './timeline';
export { default as useFilters } from './higher-order/use-filters'; export { default as useFilters } from './higher-order/use-filters';
export { default as ViewMoreList } from './view-more-list'; export { default as ViewMoreList } from './view-more-list';
export { default as WebPreview } from './web-preview'; export { default as WebPreview } from './web-preview';

View File

@ -35,5 +35,6 @@
@import 'tag/style.scss'; @import 'tag/style.scss';
@import 'text-control/style.scss'; @import 'text-control/style.scss';
@import 'text-control-with-affixes/style.scss'; @import 'text-control-with-affixes/style.scss';
@import 'timeline/style.scss';
@import 'view-more-list/style.scss'; @import 'view-more-list/style.scss';
@import 'web-preview/style.scss'; @import 'web-preview/style.scss';

View File

@ -0,0 +1,70 @@
Timeline
===
This is a timeline for displaying data, such as events, in chronological order.
It accepts `items` for the timeline content and will order the data for you.
## Usage
```jsx
import Timeline from './Timeline';
import { orderByOptions, groupByOptions } from './Timeline';
import GridIcon from 'gridicons';
const items = [
{
date: new Date( 2019, 9, 28, 9, 0 ),
icon: <GridIcon icon={ 'checkmark' } />,
headline: 'A payment of $90.00 was successfully charged',
body: [
<p key={ '1' }>{ 'Fee: $2.91 ( 2.9% + $0.30 )' }</p>,
<p key={ '2' }>{ 'Net deposit: $87.09' }</p>,
],
},
{
date: new Date( 2019, 9, 28, 9, 32 ),
icon: <GridIcon icon={ 'plus' } />,
headline: '$94.16 was added to your October 29, 2019 deposit',
body: [],
},
{
date: new Date( 2019, 9, 27, 20, 9 ),
icon: <GridIcon icon={ 'checkmark' } className={ 'is-success' } />,
headline: 'A payment of $90.00 was successfully authorized',
body: [],
},
]
<Timeline
items={ items }
groupBy={ groupByOptions.DAY }
orderBy={ orderByOptions.ASC }
/>
```
### Props
Name | Type | Default | Description
--- | --- | --- | ---
`className` | String | `''` | Additional class names that can be applied for styling purposes
`items` | Array | `[]` | An array of items to be displayed on the timeline
`orderBy` | String | `'asc'` | How the items should be ordered, either `'asc'` or `'desc'`
`groupBy` | String | `'day'` | How the items should be grouped, one of `'day'`, `'week'`, or `'month'`
`dateFormat` | String | `'F j, Y'` | PHP date format string used to format dates, see php.net/date
`clockFormat` | String | `'g:ia'` | PHP clock format string used to format times, see php.net/date
### `items` structure
A list of items with properties:
Name | Type | Default | Description
--- | --- | --- | ---
`date` | Date | Required | JavaScript Date object set to when this event happened
`icon` | Element | Required | The element used to represent the icon for this event
`headline` | Element | Required | The element used to represent the title of this event
`body` | Array | `[]` | Elements that contain details pertaining to this event
`hideTimestamp` | Bool | `false` | Allows the user to hide the timestamp associated with this event
Icon color can be customized by adding 1 of 3 classes to the icon element: `is-success` (green), `is-warning` (yellow), and `is-error` (red)
- If no class is provided the icon will be gray

View File

@ -0,0 +1,45 @@
/**
* External dependencies
*/
import GridIcon from 'gridicons';
export default [
{
date: new Date( 2020, 0, 20, 1, 30 ),
body: [ <p key={ '1' }>{ 'p element in body' }</p>, 'string in body' ],
headline: <p>{ 'p tag in headline' }</p>,
icon: (
<GridIcon
className={ 'is-success' }
icon={ 'checkmark' }
size={ 16 }
/>
),
hideTimestamp: true,
},
{
date: new Date( 2020, 0, 20, 23, 45 ),
body: [],
headline: <span>{ 'span in headline' }</span>,
icon: (
<GridIcon
className={ 'is-warning' }
icon={ 'refresh' }
size={ 16 }
/>
),
},
{
date: new Date( 2020, 0, 22, 15, 13 ),
body: [ <span key={ '1' }>{ 'span in body' }</span> ],
headline: 'string in headline',
icon: (
<GridIcon className={ 'is-error' } icon={ 'cross' } size={ 16 } />
),
},
{
date: new Date( 2020, 0, 17, 1, 45 ),
headline: 'undefined body and string headline',
icon: <GridIcon icon={ 'cross' } size={ 16 } />,
},
];

View File

@ -0,0 +1,131 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
import { format } from '@wordpress/date';
/**
* Internal dependencies
*/
import TimelineGroup from './timeline-group';
import { sortByDateUsing, groupItemsUsing } from './util';
const Timeline = ( props ) => {
const {
className,
items,
groupBy,
orderBy,
dateFormat,
clockFormat,
} = props;
const timelineClassName = classnames( 'woocommerce-timeline', className );
// Early return in case no data was passed to the component.
if ( ! items || items.length === 0 ) {
return (
<div className={ timelineClassName }>
<p className={ 'timeline_no_events' }>
{ __( 'No data to display', 'woocommerce-admin' ) }
</p>
</div>
);
}
const addGroupTitles = ( group ) => {
return {
...group,
title: format( dateFormat, group.date ),
};
};
return (
<div className={ timelineClassName }>
<ul>
{ items
.reduce( groupItemsUsing( groupBy ), [] )
.map( addGroupTitles )
.sort( sortByDateUsing( orderBy ) )
.map( ( group ) => (
<TimelineGroup
key={ group.date.getTime().toString() }
group={ group }
orderBy={ orderBy }
clockFormat={ clockFormat }
/>
) ) }
</ul>
</div>
);
};
Timeline.propTypes = {
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* An array of list items.
*/
items: PropTypes.arrayOf(
PropTypes.shape( {
/**
* Date for the timeline item.
*/
date: PropTypes.instanceOf( Date ).isRequired,
/**
* Icon for the Timeline item.
*/
icon: PropTypes.element.isRequired,
/**
* Headline displayed for the list item.
*/
headline: PropTypes.oneOfType( [
PropTypes.element,
PropTypes.string,
] ).isRequired,
/**
* Body displayed for the list item.
*/
body: PropTypes.arrayOf(
PropTypes.oneOfType( [ PropTypes.element, PropTypes.string ] )
),
/**
* Allows users to toggle the timestamp on or off.
*/
hideTimestamp: PropTypes.bool,
} )
).isRequired,
/**
* Defines how items should be grouped together.
*/
groupBy: PropTypes.oneOf( [ 'day', 'week', 'month' ] ),
/**
* Defines how groups should be ordered.
*/
orderBy: PropTypes.oneOf( [ 'asc', 'desc' ] ),
/**
* The PHP date format string used to format dates, see php.net/date.
*/
dateFormat: PropTypes.string,
/**
* The PHP clock format string used to format times, see php.net/date.
*/
clockFormat: PropTypes.string,
};
Timeline.defaultProps = {
className: '',
items: [],
groupBy: 'day',
orderBy: 'desc',
/* translators: PHP date format string used to display dates, see php.net/date. */
dateFormat: __( 'F j, Y', 'woocommerce-admin' ),
/* translators: PHP clock format string used to display times, see php.net/date. */
clockFormat: __( 'g:ia', 'woocommerce-admin' ),
};
export { orderByOptions, groupByOptions } from './util';
export default Timeline;

View File

@ -0,0 +1,107 @@
/**
* External dependencies
*/
import { date, text } from '@storybook/addon-knobs';
import GridIcon from 'gridicons';
/**
* Internal dependencies
*/
import Timeline, { orderByOptions } from '../';
export default {
title: 'WooCommerce Admin/components/Timeline',
component: Timeline,
};
export const Empty = () => <Timeline />;
const itemDate = ( label, value ) => {
const d = date( label, value );
return new Date( d );
};
export const Filled = () => (
<Timeline
orderBy={ orderByOptions.DESC }
items={ [
{
date: itemDate(
'event 1 date',
new Date( 2020, 0, 20, 1, 30 )
),
body: [
<p key={ '1' }>
{ text( 'event 1, first event', 'p element in body' ) }
</p>,
text( 'event 1, second event', 'string in body' ),
],
headline: (
<p>{ text( 'event 1, headline', 'p tag in headline' ) }</p>
),
icon: (
<GridIcon
className={ 'is-success' }
icon={ text( 'event 1 gridicon', 'checkmark' ) }
size={ 16 }
/>
),
hideTimestamp: true,
},
{
date: itemDate(
'event 2 date',
new Date( 2020, 0, 20, 23, 45 )
),
body: [],
headline: (
<span>
{ text( 'event 2, headline', 'span in headline' ) }
</span>
),
icon: (
<GridIcon
className={ 'is-warning' }
icon={ text( 'event 2 gridicon', 'refresh' ) }
size={ 16 }
/>
),
},
{
date: itemDate(
'event 3 date',
new Date( 2020, 0, 22, 15, 13 )
),
body: [
<span key={ '1' }>
{ text( 'event 3, second event', 'span in body' ) }
</span>,
],
headline: text( 'event 3, headline', 'string in headline' ),
icon: (
<GridIcon
className={ 'is-error' }
icon={ text( 'event 3 gridicon', 'cross' ) }
size={ 16 }
/>
),
},
{
date: itemDate(
'event 4 date',
new Date( 2020, 0, 17, 1, 45 )
),
headline: text(
'event 4, headline',
'undefined body and string headline'
),
icon: (
<GridIcon
icon={ text( 'event 4 gridicon', 'cross' ) }
size={ 16 }
/>
),
},
] }
/>
);

View File

@ -0,0 +1,122 @@
.woocommerce-timeline {
ul {
margin: 0;
padding-left: 0;
list-style-type: none;
li {
margin-bottom: 0;
}
}
.woocommerce-timeline-group {
.woocommerce-timeline-group__title {
color: $studio-gray-90;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
margin: 0 0 $gap 0;
// Overrides the default `display: block` for p elements in the title.
// This is done to prevent soft line breaks from appearing in shorter
// titles.
display: inline-block;
}
hr {
float: right;
width: calc(100% - #{ $gap-largest });
margin-bottom: $gap;
// Color is according to design, we should probably find a suitable color variable.
border: 0.5px solid #e3dfe2;
}
}
.woocommerce-timeline-item {
.woocommerce-timeline-item__top-border {
min-height: 16px;
border-left: 1px solid $studio-gray-10;
margin: 0 $gap-small;
}
.woocommerce-timeline-item__title {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
color: $studio-gray-80;
* {
font-size: 16px;
}
}
.woocommerce-timeline-item__headline {
display: flex;
align-items: center;
flex-direction: row;
margin: $gap-smaller 0;
* {
margin: 0;
}
& > * {
padding: 0 $gap;
}
svg {
fill: $studio-white;
padding: $gap-smallest;
background: $studio-gray-10;
border-radius: 9999px;
box-sizing: content-box;
// We hard code the size to maintain consistent styling and spacing.
width: 16px;
height: 16px;
&.is-success {
background: $valid-green;
}
&.is-warning {
background: $notice-yellow;
}
&.is-error {
background: $error-red;
}
}
}
.woocommerce-timeline-item__timestamp {
font-size: 14px;
line-height: 16px;
}
.woocommerce-timeline-item__body {
display: flex;
flex-direction: column;
color: $studio-gray-60;
margin: 0 $gap-small;
padding: $gap-smaller $gap-larger;
border-left: 1px solid $studio-gray-10;
// Make sure child elements fit tightly together.
* {
margin: 0;
font-size: 14px;
}
}
}
}
// Hide last <hr /> element.
.woocommerce-timeline ul :last-child.woocommerce-timeline-group hr:last-child {
display: none;
}

View File

@ -0,0 +1,231 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Timeline Empty snapshot 1`] = `
<div
className="woocommerce-timeline"
>
<p
className="timeline_no_events"
>
No data to display
</p>
</div>
`;
exports[`Timeline With data snapshot 1`] = `
<div
className="woocommerce-timeline"
>
<ul>
<li
className="woocommerce-timeline-group"
>
<p
className="woocommerce-timeline-group__title"
>
January 22, 2020
</p>
<ul>
<li
className="woocommerce-timeline-item"
>
<div
className="woocommerce-timeline-item__top-border"
/>
<div
className="woocommerce-timeline-item__title"
>
<div
className="woocommerce-timeline-item__headline"
>
<svg
className="gridicon gridicons-cross is-error"
height={16}
viewBox="0 0 24 24"
width={16}
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M18.36 19.78L12 13.41l-6.36 6.37-1.42-1.42L10.59 12 4.22 5.64l1.42-1.42L12 10.59l6.36-6.36 1.41 1.41L13.41 12l6.36 6.36z"
/>
</g>
</svg>
<span>
string in headline
</span>
</div>
<span
className="woocommerce-timeline-item__timestamp"
>
3:13pm
</span>
</div>
<div
className="woocommerce-timeline-item__body"
>
<span>
<span>
span in body
</span>
</span>
</div>
</li>
</ul>
<hr />
</li>
<li
className="woocommerce-timeline-group"
>
<p
className="woocommerce-timeline-group__title"
>
January 20, 2020
</p>
<ul>
<li
className="woocommerce-timeline-item"
>
<div
className="woocommerce-timeline-item__top-border"
/>
<div
className="woocommerce-timeline-item__title"
>
<div
className="woocommerce-timeline-item__headline"
>
<svg
className="gridicon gridicons-refresh is-warning"
height={16}
viewBox="0 0 24 24"
width={16}
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M17.91 14c-.478 2.833-2.943 5-5.91 5-3.308 0-6-2.692-6-6s2.692-6 6-6h2.172l-2.086 2.086L13.5 10.5 18 6l-4.5-4.5-1.414 1.414L14.172 5H12c-4.418 0-8 3.582-8 8s3.582 8 8 8c4.08 0 7.438-3.055 7.93-7h-2.02z"
/>
</g>
</svg>
<span>
<span>
span in headline
</span>
</span>
</div>
<span
className="woocommerce-timeline-item__timestamp"
>
11:45pm
</span>
</div>
<div
className="woocommerce-timeline-item__body"
/>
</li>
<li
className="woocommerce-timeline-item"
>
<div
className="woocommerce-timeline-item__top-border"
/>
<div
className="woocommerce-timeline-item__title"
>
<div
className="woocommerce-timeline-item__headline"
>
<svg
className="gridicon gridicons-checkmark is-success"
height={16}
viewBox="0 0 24 24"
width={16}
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M9 19.414l-6.707-6.707 1.414-1.414L9 16.586 20.293 5.293l1.414 1.414"
/>
</g>
</svg>
<span>
<p>
p tag in headline
</p>
</span>
</div>
<span
className="woocommerce-timeline-item__timestamp"
/>
</div>
<div
className="woocommerce-timeline-item__body"
>
<span>
<p>
p element in body
</p>
</span>
<span>
string in body
</span>
</div>
</li>
</ul>
<hr />
</li>
<li
className="woocommerce-timeline-group"
>
<p
className="woocommerce-timeline-group__title"
>
January 17, 2020
</p>
<ul>
<li
className="woocommerce-timeline-item"
>
<div
className="woocommerce-timeline-item__top-border"
/>
<div
className="woocommerce-timeline-item__title"
>
<div
className="woocommerce-timeline-item__headline"
>
<svg
className="gridicon gridicons-cross"
height={16}
viewBox="0 0 24 24"
width={16}
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M18.36 19.78L12 13.41l-6.36 6.37-1.42-1.42L10.59 12 4.22 5.64l1.42-1.42L12 10.59l6.36-6.36 1.41 1.41L13.41 12l6.36 6.36z"
/>
</g>
</svg>
<span>
undefined body and string headline
</span>
</div>
<span
className="woocommerce-timeline-item__timestamp"
>
1:45am
</span>
</div>
<div
className="woocommerce-timeline-item__body"
/>
</li>
</ul>
<hr />
</li>
</ul>
</div>
`;

View File

@ -0,0 +1,189 @@
/* eslint-disable jest/no-mocks-import */
/**
* External dependencies
*/
import { shallow, mount } from 'enzyme';
import renderer from 'react-test-renderer';
/**
* Internal dependencies
*/
import Timeline from '..';
import mockData from '../__mocks__/timeline-mock-data';
import { groupItemsUsing, sortByDateUsing } from '../util.js';
describe( 'Timeline', () => {
test( 'Renders empty correctly', () => {
const timeline = shallow( <Timeline /> );
expect( timeline.find( '.timeline_no_events' ).length ).toBe( 1 );
expect(
timeline
.find( '.timeline_no_events' )
.first()
.contains( 'No data to display' )
).toBe( true );
} );
test( 'Renders data correctly', () => {
const timeline = mount( <Timeline items={ mockData } /> );
// Ensure correct divs are loaded.
expect( timeline.find( '.timeline_no_events' ).length ).toBe( 0 );
expect( timeline.find( '.woocommerce-timeline' ).length ).toBe( 1 );
// Ensure groups have the correct number of items.
expect( timeline.find( '.woocommerce-timeline-group' ).length ).toBe(
3
);
expect(
timeline
.find( '.woocommerce-timeline-group ul' )
.at( 0 )
.children().length
).toBe( 1 );
expect(
timeline
.find( '.woocommerce-timeline-group ul' )
.at( 1 )
.children().length
).toBe( 2 );
expect(
timeline
.find( '.woocommerce-timeline-group ul' )
.at( 2 )
.children().length
).toBe( 1 );
// Ensure dates are correctly rendered.
expect(
timeline
.find( '.woocommerce-timeline-group__title' )
.first()
.text()
).toBe( 'January 22, 2020' );
expect(
timeline
.find( '.woocommerce-timeline-group__title' )
.last()
.text()
).toBe( 'January 17, 2020' );
// Ensure the correct number of items is rendered.
expect( timeline.find( '.woocommerce-timeline-item' ).length ).toBe(
4
);
// Ensure hidden timestamps are actually hidden and vice versa.
expect(
timeline
.find( '.woocommerce-timeline-item__timestamp' )
.at( 2 )
.text()
).toBe( '' );
expect(
timeline
.find( '.woocommerce-timeline-item__timestamp' )
.at( 1 )
.text()
).not.toBe( '' );
} );
test( 'Empty snapshot', () => {
const tree = renderer.create( <Timeline /> ).toJSON();
expect( tree ).toMatchSnapshot();
} );
test( 'With data snapshot', () => {
const tree = renderer
.create( <Timeline items={ mockData } /> )
.toJSON();
expect( tree ).toMatchSnapshot();
} );
describe( 'Timeline utilities', () => {
test( 'Sorts correctly', () => {
const jan21 = new Date( 2020, 0, 21 );
const jan22 = new Date( 2020, 0, 22 );
const jan23 = new Date( 2020, 0, 23 );
const data = [
{ id: 0, date: jan22 },
{ id: 1, date: jan21 },
{ id: 2, date: jan23 },
];
const expectedAsc = [
{ id: 1, date: jan21 },
{ id: 0, date: jan22 },
{ id: 2, date: jan23 },
];
const expectedDesc = [
{ id: 2, date: jan23 },
{ id: 0, date: jan22 },
{ id: 1, date: jan21 },
];
expect( data.sort( sortByDateUsing( 'asc' ) ) ).toStrictEqual(
expectedAsc
);
expect( data.sort( sortByDateUsing( 'desc' ) ) ).toStrictEqual(
expectedDesc
);
} );
test( "Empty item list doesn't break sort", () => {
expect( [].sort( sortByDateUsing( 'asc' ) ) ).toStrictEqual( [] );
} );
test( "Single item doesn't change on sort", () => {
const items = [ { date: new Date( 2020, 0, 1 ) } ];
expect( items.sort( sortByDateUsing( 'asc' ) ) ).toBe( items );
} );
test( 'Groups correctly', () => {
const jan22 = new Date( 2020, 0, 22 );
const jan23 = new Date( 2020, 0, 23 );
const items = [
{ id: 0, date: jan22 },
{ id: 1, date: jan23 },
{ id: 2, date: jan22 },
];
const expected = [
{
date: jan22,
items: [
{ id: 0, date: jan22 },
{ id: 2, date: jan22 },
],
},
{
date: jan23,
items: [ { id: 1, date: jan23 } ],
},
];
expect(
items.reduce( groupItemsUsing( 'days' ), [] )
).toStrictEqual( expected );
} );
test( "Empty item list doesn't break grouping", () => {
expect( [].reduce( groupItemsUsing( 'days' ), [] ) ).toStrictEqual(
[]
);
} );
test( 'Single item grouped correctly', () => {
const jan22 = new Date( 2020, 0, 22 );
const items = [ { id: 0, date: jan22 } ];
const expected = [
{
date: jan22,
items: [ { id: 0, date: jan22 } ],
},
];
expect(
items.reduce( groupItemsUsing( 'days' ), [] )
).toStrictEqual( expected );
} );
} );
} );

View File

@ -0,0 +1,113 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import TimelineItem from './timeline-item';
import { sortByDateUsing } from './util';
const TimelineGroup = ( props ) => {
const { group, className, orderBy, clockFormat } = props;
const groupClassName = classnames(
'woocommerce-timeline-group',
className
);
const itemsToTimlineItem = ( item, itemIndex ) => {
const itemKey = group.title + '-' + itemIndex;
return (
<TimelineItem
key={ itemKey }
item={ item }
clockFormat={ clockFormat }
/>
);
};
return (
<li className={ groupClassName }>
<p className={ 'woocommerce-timeline-group__title' }>
{ group.title }
</p>
<ul>
{ group.items
.sort( sortByDateUsing( orderBy ) )
.map( itemsToTimlineItem ) }
</ul>
<hr />
</li>
);
};
TimelineGroup.propTypes = {
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* The group to render.
*/
group: PropTypes.shape( {
/**
* The group title.
*/
title: PropTypes.string,
/**
* An array of list items.
*/
items: PropTypes.arrayOf(
PropTypes.shape( {
/**
* Date for the timeline item.
*/
date: PropTypes.instanceOf( Date ).isRequired,
/**
* Icon for the Timeline item.
*/
icon: PropTypes.element.isRequired,
/**
* Headline displayed for the list item.
*/
headline: PropTypes.oneOfType( [
PropTypes.element,
PropTypes.string,
] ).isRequired,
/**
* Body displayed for the list item.
*/
body: PropTypes.arrayOf(
PropTypes.oneOfType( [
PropTypes.element,
PropTypes.string,
] )
),
/**
* Allows users to toggle the timestamp on or off.
*/
hideTimestamp: PropTypes.bool,
} )
).isRequired,
} ).isRequired,
/**
* Defines how items should be ordered.
*/
orderBy: PropTypes.oneOf( [ 'asc', 'desc' ] ),
/**
* The PHP clock format string used to format times, see php.net/date.
*/
clockFormat: PropTypes.string,
};
TimelineGroup.defaultProps = {
className: '',
group: {
title: '',
items: [],
},
orderBy: 'desc',
};
export default TimelineGroup;

View File

@ -0,0 +1,81 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { format } from '@wordpress/date';
import PropTypes from 'prop-types';
const TimelineItem = ( props ) => {
const { item, className, clockFormat } = props;
const itemClassName = classnames( 'woocommerce-timeline-item', className );
const itemTimeString = format( clockFormat, item.date );
return (
<li className={ itemClassName }>
<div className={ 'woocommerce-timeline-item__top-border' }></div>
<div className={ 'woocommerce-timeline-item__title' }>
<div className={ 'woocommerce-timeline-item__headline' }>
{ item.icon }
<span>{ item.headline }</span>
</div>
<span className={ 'woocommerce-timeline-item__timestamp' }>
{ item.hideTimestamp || false ? null : itemTimeString }
</span>
</div>
<div className={ 'woocommerce-timeline-item__body' }>
{ ( item.body || [] ).map( ( bodyItem, index ) => (
<span key={ `timeline-item-body-${ index }` }>
{ bodyItem }
</span>
) ) }
</div>
</li>
);
};
TimelineItem.propTypes = {
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* An array of list items.
*/
item: PropTypes.shape( {
/**
* Date for the timeline item.
*/
date: PropTypes.instanceOf( Date ).isRequired,
/**
* Icon for the Timeline item.
*/
icon: PropTypes.element.isRequired,
/**
* Headline displayed for the list item.
*/
headline: PropTypes.oneOfType( [ PropTypes.element, PropTypes.string ] )
.isRequired,
/**
* Body displayed for the list item.
*/
body: PropTypes.arrayOf(
PropTypes.oneOfType( [ PropTypes.element, PropTypes.string ] )
),
/**
* Allows users to toggle the timestamp on or off.
*/
hideTimestamp: PropTypes.bool,
/**
* The PHP clock format string used to format times, see php.net/date.
*/
clockFormat: PropTypes.string,
} ).isRequired,
};
TimelineItem.defaultProps = {
className: '',
item: {},
};
export default TimelineItem;

View File

@ -0,0 +1,57 @@
/**
* External dependencies
*/
import moment from 'moment';
const orderByOptions = {
ASC: 'asc',
DESC: 'desc',
};
const groupByOptions = {
DAY: 'day',
WEEK: 'week',
MONTH: 'month',
};
const sortAscending = ( groupA, groupB ) =>
groupA.date.getTime() - groupB.date.getTime();
const sortDescending = ( groupA, groupB ) =>
groupB.date.getTime() - groupA.date.getTime();
const sortByDateUsing = ( orderBy ) => {
switch ( orderBy ) {
case orderByOptions.ASC:
return sortAscending;
case orderByOptions.DESC:
default:
return sortDescending;
}
};
const groupItemsUsing = ( groupBy ) => ( groups, newItem ) => {
// Helper functions defined to make the logic a bit more readable.
const hasSameMoment = ( group, item ) => {
return moment( group.date ).isSame( moment( item.date ), groupBy );
};
const groupIndexExists = ( index ) => index >= 0;
const groupForItem = groups.findIndex( ( group ) =>
hasSameMoment( group, newItem )
);
if ( ! groupIndexExists( groupForItem ) ) {
// Create new group for newItem.
return [
...groups,
{
date: newItem.date,
items: [ newItem ],
},
];
}
groups[ groupForItem ].items.push( newItem );
return groups;
};
export { groupByOptions, groupItemsUsing, orderByOptions, sortByDateUsing };