-
Notifications
You must be signed in to change notification settings - Fork 87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create wp.data store for retrieving/tracking events #332
Changes from all commits
2fbab6d
011b377
7c50120
92966d2
4c3ea41
7d94cb0
c44c125
e48a152
5a106fa
48358e3
8ea3e48
3420132
6e160b9
d3e8642
b08ad45
773fb65
29e303f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { reduce } from 'lodash'; | ||
|
||
/** | ||
* Receives an array of event entities and returns an array of simple objects | ||
* that can be passed along to the options array used for the WordPress | ||
* SelectControl component. | ||
* | ||
* @param { Array } events | ||
* @return { Array } Returns an array of simple objects formatted for the | ||
* WordPress SelectControl component. | ||
*/ | ||
export const buildEventOptions = ( events ) => { | ||
return reduce( events, function( options, event ) { | ||
options.push( | ||
{ | ||
label: event.EVT_name, | ||
value: event.EVT_ID, | ||
}, | ||
); | ||
return options; | ||
}, [] ); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,77 +3,79 @@ | |
*/ | ||
import { stringify } from 'querystringify'; | ||
import moment from 'moment'; | ||
import { isUndefined, pickBy, reduce, isEmpty } from 'lodash'; | ||
import { isUndefined, pickBy, isEmpty } from 'lodash'; | ||
import PropTypes from 'prop-types'; | ||
|
||
/** | ||
* WP dependencies | ||
*/ | ||
const { Component } = wp.element; | ||
const { Placeholder, SelectControl, withAPIData, Spinner } = wp.components; | ||
const { __ } = wp.i18n; | ||
import { Placeholder, SelectControl, Spinner } from '@wordpress/components'; | ||
import { __ } from '@wordpress/i18n'; | ||
import { withSelect } from '@wordpress/data'; | ||
|
||
const nowDateAndTime = moment(); | ||
|
||
const buildEventOptions = ( events ) => { | ||
reduce( events, function( options, event ) { | ||
options.push( | ||
{ | ||
label: event.EVT_name, | ||
value: event.EVT_ID, | ||
}, | ||
); | ||
return options; | ||
}, [] ); | ||
}; | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { buildEventOptions } from './build-event-options'; | ||
|
||
export class EventSelect extends Component { | ||
render() { | ||
const { | ||
events = [], | ||
onEventSelect, | ||
selectLabel = __( 'Select Event' ), | ||
selectedEventId, | ||
isLoading = true, | ||
} = this.props; | ||
if ( isLoading || isEmpty( events ) ) { | ||
return <Placeholder key="placeholder" | ||
icon="calendar" | ||
label={ __( 'EventSelect' ) } | ||
> | ||
{ isLoading ? | ||
<Spinner /> : | ||
__( | ||
'There are no events to select from. You need to create an event first.', | ||
'event_espresso' | ||
) | ||
} | ||
</Placeholder>; | ||
} | ||
const nowDateAndTime = moment(); | ||
|
||
return <SelectControl | ||
label={ selectLabel } | ||
value={ selectedEventId } | ||
options={ buildEventOptions( events ) } | ||
onChange={ ( value ) => onEventSelect( value ) } | ||
/>; | ||
/** | ||
* EventSelect component. | ||
* A react component for an event selector. | ||
* | ||
* @param {Array} events An empty array or array of Event Entities. See | ||
* prop-types for shape. | ||
* @param {function} onEventSelect The callback on selection of event. | ||
* @param {string} selectLabel The label for the select input. | ||
* @param {number} selectedEventId If provided, the id of the event to | ||
* pre-select. | ||
* @param {boolean} isLoading Whether or not the selector should start in a | ||
* loading state | ||
* @return {Function} A pure component function. | ||
* @constructor | ||
*/ | ||
export const EventSelect = ( { | ||
events, | ||
onEventSelect, | ||
selectLabel, | ||
selectedEventId, | ||
isLoading, | ||
} ) => { | ||
if ( isLoading || isEmpty( events ) ) { | ||
return <Placeholder key="placeholder" | ||
icon="calendar" | ||
label={ __( 'EventSelect', 'event_espresso' ) } | ||
> | ||
{ isLoading ? | ||
<Spinner /> : | ||
__( | ||
'There are no events to select from. You need to create an event first.', | ||
'event_espresso', | ||
) | ||
} | ||
</Placeholder>; | ||
} | ||
} | ||
|
||
return <SelectControl | ||
label={ selectLabel } | ||
value={ selectedEventId } | ||
options={ buildEventOptions( events ) } | ||
onChange={ ( value ) => onEventSelect( value ) } | ||
/>; | ||
}; | ||
|
||
/** | ||
* @todo some of these proptypes are likely reusable in various place so we may | ||
* want to consider extracting them into a separate file/object that can be | ||
* included as needed. | ||
* @type {{events: *, onEventSelect, selectLabel: *, selectedEventId: *, | ||
* isLoading: *, attributes: {limit: *, orderBy: *, order: *, showExpired: *, | ||
* categorySlug: *, month: *}}} | ||
*/ | ||
EventSelect.propTypes = { | ||
events: PropTypes.shape( { | ||
events: PropTypes.arrayOf( PropTypes.shape( { | ||
EVT_name: PropTypes.string.required, | ||
EVT_ID: PropTypes.number.required, | ||
} ), | ||
onEventSelect: PropTypes.func.required, | ||
} ) ), | ||
onEventSelect: PropTypes.func, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. made this not required because there's validation upstream within the |
||
selectLabel: PropTypes.string, | ||
selectedEventId: PropTypes.number, | ||
isLoading: PropTypes.bool, | ||
|
@@ -101,6 +103,9 @@ EventSelect.defaultProps = { | |
order: 'desc', | ||
showExpired: false, | ||
}, | ||
selectLabel: __( 'Select Event', 'event_espresso' ), | ||
isLoading: true, | ||
events: [], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved the defaults here instead of in the constructor for the component (since I was already defining defaults here). |
||
}; | ||
|
||
/** | ||
|
@@ -112,7 +117,7 @@ EventSelect.defaultProps = { | |
* @param {string} orderBy | ||
* | ||
* @return { string } Returns an actual orderBy string for the REST query for | ||
* the provided alias | ||
* the provided alias | ||
*/ | ||
const mapOrderBy = ( orderBy ) => { | ||
const orderByMap = { | ||
|
@@ -121,9 +126,21 @@ const mapOrderBy = ( orderBy ) => { | |
ticket_start: 'Datetime.Ticket.TKT_start_date', | ||
ticket_end: 'Datetime.Ticket.TKT_end_date', | ||
}; | ||
return isUndefined( orderByMap[ orderBy ] ) ? orderBy : orderByMap[ orderBy ]; | ||
return isUndefined( orderByMap[ orderBy ] ) ? | ||
orderBy : | ||
orderByMap[ orderBy ]; | ||
}; | ||
|
||
/** | ||
* Builds where conditions for an events endpoint request using provided | ||
* information. | ||
* | ||
* @param {boolean} showExpired Whether or not to include expired events. | ||
* @param {string} categorySlug Return events for the given categorySlug | ||
* @param {string} month Return events for the given month. Can be any | ||
* in any month format recognized by moment. | ||
* @return {string} The assembled where conditions. | ||
*/ | ||
const whereConditions = ( { showExpired, categorySlug, month } ) => { | ||
const where = []; | ||
const GREATER_AND_EQUAL = encodeURIComponent( '>=' ); | ||
|
@@ -137,17 +154,27 @@ const whereConditions = ( { showExpired, categorySlug, month } ) => { | |
where.push( 'where[Term_Relationship.Term_Taxonomy.Term.slug]=' + categorySlug ); | ||
} | ||
if ( month && month !== 'none' ) { | ||
where.push( 'where[Datetime.DTT_EVT_start][]=' + GREATER_AND_EQUAL + '&where[Datetime.DTT_EVT_start][]=' + | ||
where.push( 'where[Datetime.DTT_EVT_start][]=' + | ||
GREATER_AND_EQUAL + | ||
'&where[Datetime.DTT_EVT_start][]=' + | ||
moment().month( month ).startOf( 'month' ).local().format() ); | ||
where.push( 'where[Datetime.DTT_EVT_end][]=' + LESS_AND_EQUAL + '&where[Datetime.DTT_EVT_end][]=' + | ||
where.push( 'where[Datetime.DTT_EVT_end][]=' + | ||
LESS_AND_EQUAL + | ||
'&where[Datetime.DTT_EVT_end][]=' + | ||
moment().month( month ).endOf( 'month' ).local().format() ); | ||
} | ||
return where.join( '&' ); | ||
}; | ||
|
||
export default withAPIData( ( props ) => { | ||
const { limit, order, orderBy } = props.attributes; | ||
const where = whereConditions( props.attributes ); | ||
/** | ||
* The EventSelect Component wrapped in the `withSelect` higher order component. | ||
* This subscribes the EventSelect component to the state maintained via the | ||
* eventespresso/lists store. | ||
*/ | ||
export default withSelect( ( select, ownProps ) => { | ||
const { limit, order, orderBy } = ownProps.attributes; | ||
const where = whereConditions( ownProps.attributes ); | ||
const { getEvents, isRequestingEvents } = select( 'eventespresso/lists' ); | ||
const queryArgs = { | ||
limit, | ||
order, | ||
|
@@ -160,9 +187,8 @@ export default withAPIData( ( props ) => { | |
if ( where ) { | ||
queryString += '&' + where; | ||
} | ||
|
||
return { | ||
events: `/ee/v4.8.36/events?${ queryString }`, | ||
isLoading: false, | ||
events: getEvents( queryString ), | ||
isLoading: isRequestingEvents( queryString ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is implementing the new HOC that was done in this branch. |
||
}; | ||
} )( EventSelect ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`EventSelect with 2 events should render and match snapshot 1`] = ` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a snapshot created by Jest and the new tests I added. This should never get modified directly. |
||
<_class | ||
label="Select Event" | ||
onChange={[Function]} | ||
options={ | ||
Array [ | ||
Object { | ||
"label": "Event A", | ||
"value": 1, | ||
}, | ||
Object { | ||
"label": "Event B", | ||
"value": 2, | ||
}, | ||
] | ||
} | ||
/> | ||
`; | ||
|
||
exports[`EventSelect with default options should render and match snapshot 1`] = ` | ||
<Placeholder | ||
icon="calendar" | ||
key="placeholder" | ||
label="EventSelect" | ||
> | ||
<Component /> | ||
</Placeholder> | ||
`; | ||
|
||
exports[`EventSelect with no events and finished loading should render and match snapshot 1`] = ` | ||
<Placeholder | ||
icon="calendar" | ||
key="placeholder" | ||
label="EventSelect" | ||
> | ||
There are no events to select from. You need to create an event first. | ||
</Placeholder> | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { shallow, render } from 'enzyme'; | ||
import { EventSelect } from '../index'; | ||
|
||
const simulatedEventOptions = [ | ||
{ EVT_ID: 1, EVT_name: 'Event A' }, | ||
{ EVT_ID: 2, EVT_name: 'Event B' }, | ||
]; | ||
|
||
describe( 'EventSelect with default options', () => { | ||
it( 'should render and match snapshot', () => { | ||
const selector = shallow( <EventSelect /> ); | ||
expect( selector ).toMatchSnapshot(); | ||
} ); | ||
} ); | ||
|
||
describe( 'EventSelect with no events and finished loading', () => { | ||
it( 'should render and match snapshot', () => { | ||
const selector = shallow( <EventSelect isLoading={ false } /> ); | ||
expect( selector ).toMatchSnapshot(); | ||
} ); | ||
} ); | ||
|
||
describe( 'EventSelect with 2 events', () => { | ||
const element = <EventSelect | ||
isLoading={ false } | ||
events={ simulatedEventOptions } | ||
onEventSelect={ jest.fn() } | ||
/>; | ||
it( 'should render and match snapshot', () => { | ||
const selector = shallow( element ); | ||
expect( selector ).toMatchSnapshot(); | ||
} ); | ||
it( 'should render and have 2 options', () => { | ||
const selector = render( element ); | ||
expect( selector.find( 'option' ) ).toHaveLength( 2 ); | ||
} ); | ||
|
||
it( 'should render and the first option has label and value matching the first siumlated event', | ||
() => { | ||
const selector = render( element ); | ||
const firstOption = selector.find( 'option' ).first(); | ||
expect( firstOption.text() ) | ||
.toEqual( simulatedEventOptions[ 0 ].EVT_name ); | ||
expect( firstOption.val() ) | ||
.toEqual( simulatedEventOptions[ 0 ].EVT_ID.toString() ); | ||
} | ||
); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original implementation of this was incorrect, so this fixes that (covered by tests as well).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's very likely that we will repeatedly use this kind of object for various selectors, what about using a more formally declared class ?
This could even be an abstract parent with concrete children for defining the specific types. ie:
otherwise we will have to import the prop-types package and write this same code over and over in every component that uses selectors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no idea how this class will take shape yet. I'm also not sure we'd need a class yet. I'd like to avoid doing anything until we see patterns emerge. I think it's too early to yet.