From fe865b0c845c391409519d370509ed9ca54bb512 Mon Sep 17 00:00:00 2001 From: Valerie K Date: Wed, 17 Jun 2020 04:44:08 +0900 Subject: [PATCH] Add Timeline component (https://github.com/woocommerce/woocommerce-admin/pull/3614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Allen Snook Co-authored-by: Kristófer R Co-authored-by: David Levin --- .../packages/components/CHANGELOG.md | 1 + .../packages/components/src/index.js | 1 + .../packages/components/src/style.scss | 1 + .../components/src/timeline/README.md | 70 ++++++ .../timeline/__mocks__/timeline-mock-data.js | 45 ++++ .../packages/components/src/timeline/index.js | 131 ++++++++++ .../components/src/timeline/stories/index.js | 107 ++++++++ .../components/src/timeline/style.scss | 122 +++++++++ .../timeline/test/__snapshots__/index.js.snap | 231 ++++++++++++++++++ .../components/src/timeline/test/index.js | 189 ++++++++++++++ .../components/src/timeline/timeline-group.js | 113 +++++++++ .../components/src/timeline/timeline-item.js | 81 ++++++ .../packages/components/src/timeline/util.js | 57 +++++ 13 files changed, 1149 insertions(+) create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/README.md create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/__mocks__/timeline-mock-data.js create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/stories/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/style.scss create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/test/__snapshots__/index.js.snap create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/test/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/timeline-group.js create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/timeline-item.js create mode 100644 plugins/woocommerce-admin/packages/components/src/timeline/util.js diff --git a/plugins/woocommerce-admin/packages/components/CHANGELOG.md b/plugins/woocommerce-admin/packages/components/CHANGELOG.md index e8c017d386d..7ce42d9293f 100644 --- a/plugins/woocommerce-admin/packages/components/CHANGELOG.md +++ b/plugins/woocommerce-admin/packages/components/CHANGELOG.md @@ -1,4 +1,5 @@ # 5.0.0 (Unreleased) +- Added `` component. - Added `` component. - Style form components for WordPress 5.3. - Fix CompareFilter options format (key prop vs. id). diff --git a/plugins/woocommerce-admin/packages/components/src/index.js b/plugins/woocommerce-admin/packages/components/src/index.js index 5a0e7c60efe..fcf1c3f01c8 100644 --- a/plugins/woocommerce-admin/packages/components/src/index.js +++ b/plugins/woocommerce-admin/packages/components/src/index.js @@ -57,6 +57,7 @@ export { default as TableSummary } from './table/summary'; export { default as Tag } from './tag'; export { default as TextControl } from './text-control'; 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 ViewMoreList } from './view-more-list'; export { default as WebPreview } from './web-preview'; diff --git a/plugins/woocommerce-admin/packages/components/src/style.scss b/plugins/woocommerce-admin/packages/components/src/style.scss index 209ae6ff892..329eef51470 100644 --- a/plugins/woocommerce-admin/packages/components/src/style.scss +++ b/plugins/woocommerce-admin/packages/components/src/style.scss @@ -35,5 +35,6 @@ @import 'tag/style.scss'; @import 'text-control/style.scss'; @import 'text-control-with-affixes/style.scss'; +@import 'timeline/style.scss'; @import 'view-more-list/style.scss'; @import 'web-preview/style.scss'; diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/README.md b/plugins/woocommerce-admin/packages/components/src/timeline/README.md new file mode 100644 index 00000000000..f27300da7bc --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/README.md @@ -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: , + headline: 'A payment of $90.00 was successfully charged', + body: [ +

{ 'Fee: $2.91 ( 2.9% + $0.30 )' }

, +

{ 'Net deposit: $87.09' }

, + ], + }, + { + date: new Date( 2019, 9, 28, 9, 32 ), + icon: , + headline: '$94.16 was added to your October 29, 2019 deposit', + body: [], + }, + { + date: new Date( 2019, 9, 27, 20, 9 ), + icon: , + headline: 'A payment of $90.00 was successfully authorized', + body: [], + }, +] + + +``` + +### 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 diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/__mocks__/timeline-mock-data.js b/plugins/woocommerce-admin/packages/components/src/timeline/__mocks__/timeline-mock-data.js new file mode 100644 index 00000000000..0e6902da18a --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/__mocks__/timeline-mock-data.js @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import GridIcon from 'gridicons'; + +export default [ + { + date: new Date( 2020, 0, 20, 1, 30 ), + body: [

{ 'p element in body' }

, 'string in body' ], + headline:

{ 'p tag in headline' }

, + icon: ( + + ), + hideTimestamp: true, + }, + { + date: new Date( 2020, 0, 20, 23, 45 ), + body: [], + headline: { 'span in headline' }, + icon: ( + + ), + }, + { + date: new Date( 2020, 0, 22, 15, 13 ), + body: [ { 'span in body' } ], + headline: 'string in headline', + icon: ( + + ), + }, + { + date: new Date( 2020, 0, 17, 1, 45 ), + headline: 'undefined body and string headline', + icon: , + }, +]; diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/index.js b/plugins/woocommerce-admin/packages/components/src/timeline/index.js new file mode 100644 index 00000000000..a6540d90a7e --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/index.js @@ -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 ( +
+

+ { __( 'No data to display', 'woocommerce-admin' ) } +

+
+ ); + } + + const addGroupTitles = ( group ) => { + return { + ...group, + title: format( dateFormat, group.date ), + }; + }; + + return ( +
+
    + { items + .reduce( groupItemsUsing( groupBy ), [] ) + .map( addGroupTitles ) + .sort( sortByDateUsing( orderBy ) ) + .map( ( group ) => ( + + ) ) } +
+
+ ); +}; + +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; diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/stories/index.js b/plugins/woocommerce-admin/packages/components/src/timeline/stories/index.js new file mode 100644 index 00000000000..a995ea3fd18 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/stories/index.js @@ -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 = () => ; + +const itemDate = ( label, value ) => { + const d = date( label, value ); + return new Date( d ); +}; + +export const Filled = () => ( + + { text( 'event 1, first event', 'p element in body' ) } +

, + text( 'event 1, second event', 'string in body' ), + ], + headline: ( +

{ text( 'event 1, headline', 'p tag in headline' ) }

+ ), + icon: ( + + ), + hideTimestamp: true, + }, + { + date: itemDate( + 'event 2 date', + new Date( 2020, 0, 20, 23, 45 ) + ), + body: [], + headline: ( + + { text( 'event 2, headline', 'span in headline' ) } + + ), + icon: ( + + ), + }, + { + date: itemDate( + 'event 3 date', + new Date( 2020, 0, 22, 15, 13 ) + ), + body: [ + + { text( 'event 3, second event', 'span in body' ) } + , + ], + headline: text( 'event 3, headline', 'string in headline' ), + icon: ( + + ), + }, + { + date: itemDate( + 'event 4 date', + new Date( 2020, 0, 17, 1, 45 ) + ), + headline: text( + 'event 4, headline', + 'undefined body and string headline' + ), + icon: ( + + ), + }, + ] } + /> +); diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/style.scss b/plugins/woocommerce-admin/packages/components/src/timeline/style.scss new file mode 100644 index 00000000000..c51bff8cfe5 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/style.scss @@ -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
element. +.woocommerce-timeline ul :last-child.woocommerce-timeline-group hr:last-child { + display: none; +} diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/test/__snapshots__/index.js.snap b/plugins/woocommerce-admin/packages/components/src/timeline/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000..40da12e54b3 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/test/__snapshots__/index.js.snap @@ -0,0 +1,231 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Timeline Empty snapshot 1`] = ` +
+

+ No data to display +

+
+`; + +exports[`Timeline With data snapshot 1`] = ` +
+
    +
  • +

    + January 22, 2020 +

    +
      +
    • +
      +
      +
      + + + + + + + string in headline + +
      + + 3:13pm + +
      +
      + + + span in body + + +
      +
    • +
    +
    +
  • +
  • +

    + January 20, 2020 +

    +
      +
    • +
      +
      +
      + + + + + + + + span in headline + + +
      + + 11:45pm + +
      +
      +
    • +
    • +
      +
      +
      + + + + + + +

      + p tag in headline +

      +
      +
      + +
      +
      + +

      + p element in body +

      +
      + + string in body + +
      +
    • +
    +
    +
  • +
  • +

    + January 17, 2020 +

    +
      +
    • +
      +
      +
      + + + + + + + undefined body and string headline + +
      + + 1:45am + +
      +
      +
    • +
    +
    +
  • +
+
+`; diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/test/index.js b/plugins/woocommerce-admin/packages/components/src/timeline/test/index.js new file mode 100644 index 00000000000..ba75b1f5157 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/test/index.js @@ -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( ); + 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( ); + + // 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( ).toJSON(); + expect( tree ).toMatchSnapshot(); + } ); + + test( 'With data snapshot', () => { + const tree = renderer + .create( ) + .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 ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/timeline-group.js b/plugins/woocommerce-admin/packages/components/src/timeline/timeline-group.js new file mode 100644 index 00000000000..717d984c5e2 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/timeline-group.js @@ -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 ( + + ); + }; + + return ( +
  • +

    + { group.title } +

    +
      + { group.items + .sort( sortByDateUsing( orderBy ) ) + .map( itemsToTimlineItem ) } +
    +
    +
  • + ); +}; + +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; diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/timeline-item.js b/plugins/woocommerce-admin/packages/components/src/timeline/timeline-item.js new file mode 100644 index 00000000000..43dc83ebe39 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/timeline-item.js @@ -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 ( +
  • +
    +
    +
    + { item.icon } + { item.headline } +
    + + { item.hideTimestamp || false ? null : itemTimeString } + +
    +
    + { ( item.body || [] ).map( ( bodyItem, index ) => ( + + { bodyItem } + + ) ) } +
    +
  • + ); +}; + +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; diff --git a/plugins/woocommerce-admin/packages/components/src/timeline/util.js b/plugins/woocommerce-admin/packages/components/src/timeline/util.js new file mode 100644 index 00000000000..5f58b71cfc9 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/timeline/util.js @@ -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 };