Skip to content
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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
],
"plugins": [
"transform-object-rest-spread",
"transform-react-jsx"
"transform-react-jsx",
"transform-async-generator-functions"
],
"env": {
"test": {
"presets" : [["env"]]
"presets" : [
"@wordpress/default"
]
},
"production": {
"plugins": [
Expand All @@ -24,7 +27,8 @@
}
],
"transform-object-rest-spread",
"transform-react-jsx"
"transform-react-jsx",
"transform-async-generator-functions"
]
}
}
Expand Down
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;
}, [] );
};
152 changes: 89 additions & 63 deletions assets/src/components/selection/event-select/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( {
Copy link
Contributor Author

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).

Copy link
Member

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:

  • abstract SelectorKeyValuePair class
  • EventSelectorKeyValuePair extends SelectorKeyValuePair
  • DatetimeSelectorKeyValuePair extends SelectorKeyValuePair
  • AttendeeSelectorKeyValuePair extends SelectorKeyValuePair
  • etc

otherwise we will have to import the prop-types package and write this same code over and over in every component that uses selectors.

Copy link
Contributor Author

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.

EVT_name: PropTypes.string.required,
EVT_ID: PropTypes.number.required,
} ),
onEventSelect: PropTypes.func.required,
} ) ),
onEventSelect: PropTypes.func,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made this not required because there's validation upstream within the SelectControl component.

selectLabel: PropTypes.string,
selectedEventId: PropTypes.number,
isLoading: PropTypes.bool,
Expand Down Expand Up @@ -101,6 +103,9 @@ EventSelect.defaultProps = {
order: 'desc',
showExpired: false,
},
selectLabel: __( 'Select Event', 'event_espresso' ),
isLoading: true,
events: [],
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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).

};

/**
Expand All @@ -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 = {
Expand All @@ -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( '>=' );
Expand All @@ -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,
Expand All @@ -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 ),
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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`] = `
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>
`;
48 changes: 48 additions & 0 deletions assets/src/components/selection/event-select/test/index.js
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() );
}
);
} );
Loading