OrderPage fetches order data and passes that to OrderDetailsPanel

This commit is contained in:
Vesa Luusua 2017-04-26 16:26:49 +03:00
parent b0f06f4b9a
commit 5f09c58a86
9 changed files with 251 additions and 77 deletions

View file

@ -6,7 +6,16 @@ exports[`OrderDetailsPanel matches snapshot 1`] = `
id="OrderDetailsPanel.listingTitle"
values={
Object {
"title": "listing1 title",
"title": <withFlattenedRoutes(withRouter(NamedLink))
name="ListingPage"
params={
Object {
"id": "listing1",
"slug": "listing1-title",
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
}
} />
</h1>

View file

@ -1,3 +1,7 @@
.title {
margin: 1rem 1rem 2rem 1rem;
}
.tabContent {
display: none;
}

View file

@ -0,0 +1,73 @@
import { types } from '../../util/sdkLoader';
import { addEntities } from '../../ducks/sdk.duck';
import { fetchCurrentUser } from '../../ducks/user.duck';
// ================ Action types ================ //
export const FETCH_ORDER_REQUEST = 'app/InboxPage/FETCH_ORDER_REQUEST';
export const FETCH_ORDER_SUCCESS = 'app/InboxPage/FETCH_ORDER_SUCCESS';
export const FETCH_ORDER_ERROR = 'app/InboxPage/FETCH_ORDER_ERROR';
// ================ Reducer ================ //
const initialState = {
fetchInProgress: false,
fetchOrderError: null,
transactionRef: null,
};
export default function checkoutPageReducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case FETCH_ORDER_REQUEST:
return { ...state, fetchInProgress: true, fetchOrderError: null };
case FETCH_ORDER_SUCCESS: {
const transactionRef = { id: payload.data.data.id, type: 'transaction' };
return { ...state, fetchInProgress: false, transactionRef };
}
case FETCH_ORDER_ERROR:
console.error(payload); // eslint-disable-line
return { ...state, fetchInProgress: false, fetchOrderError: payload };
default:
return state;
}
}
// ================ Action creators ================ //
const fetchOrderRequest = () => ({ type: FETCH_ORDER_REQUEST });
const fetchOrderSuccess = response => ({ type: FETCH_ORDER_SUCCESS, payload: response });
const fetchOrderError = e => ({ type: FETCH_ORDER_ERROR, error: true, payload: e });
// ================ Thunks ================ //
export const fetchOrder = id =>
(dispatch, getState, sdk) => {
dispatch(fetchOrderRequest());
return sdk.transactions
.show({ id, include: ['provider', 'customer', 'listing', 'booking'] }, { expand: true })
.then(response => {
dispatch(addEntities(response));
dispatch(fetchOrderSuccess(response));
return response;
})
.catch(e => {
dispatch(fetchOrderError(e));
throw e;
});
};
// loadData is a collection of async calls that need to be made
// before page has all the info it needs to render itself
export const loadData = params =>
dispatch => {
const orderId = new types.UUID(params.id);
// Current user is needed to render Topbar
dispatch(fetchCurrentUser());
// Order (i.e. transaction entity in API, but from buyers perspective) contains order details
return dispatch(fetchOrder(orderId));
};

View file

@ -1,62 +1,100 @@
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import { OrderDetailsPanel, OrderDiscussionPanel, NamedLink, PageLayout } from '../../components';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { OrderDetailsPanel, PageLayout } from '../../components';
import * as propTypes from '../../util/propTypes';
import { getEntities } from '../../ducks/sdk.duck';
import { loadData } from './OrderPage.duck';
import css from './OrderPage.css';
const OrderPage = props => {
const { params } = props;
const orderId = params.id;
// Create shell objects to ensure that attributes etc. exists.
// TODO: these could be moved to separate util file, if needed elsewhere
const ensureTransaction = transaction => {
const empty = {
id: null,
type: 'transaction',
attributes: {},
booking: {},
listing: {},
provider: {},
};
// assume own properties: id, type, attributes etc.
return { ...empty, ...transaction };
};
const title = 'Banyan Studios';
const ensureBooking = booking => {
const empty = { id: null, type: 'booking', attributes: {} };
return { ...empty, ...booking };
};
const ensureListing = listing => {
const empty = { id: null, type: 'listing', attributes: {}, images: [] };
return { ...empty, ...listing };
};
const ensureUser = user => {
const empty = { id: null, type: 'user', attributes: { profile: {} } };
return { ...empty, ...user };
};
// OrderPage handles data loading
// It show loading data text or OrderDetailsPanel (and later also another panel for messages).
export const OrderPageComponent = props => {
const { intl, transaction } = props;
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
const title = currentListing.attributes.title;
const detailsProps = {
title,
orderId,
imageUrl: 'http://placehold.it/750x470',
contact: {
addressLine1: '350 5th Avenue',
addressLine2: 'New York, NY 10118',
phoneNumber: '+1 432 43184910',
},
confirmationCode: 'X2587X',
totalPrice: currentTransaction.attributes.total,
orderState: currentTransaction.attributes.state,
listing: currentListing,
booking: ensureBooking(currentTransaction.booking),
provider: ensureUser(currentTransaction.provider),
};
const detailsClassName = classNames(css.tabContent, {
[css.tabContentVisible]: props.tab === 'details',
});
const discussionClassName = classNames(css.tabContent, {
[css.tabContentVisible]: props.tab === 'discussion',
});
const panel = currentTransaction.id
? <OrderDetailsPanel className={detailsClassName} {...detailsProps} />
: <h1 className={css.title}><FormattedMessage id="OrderPage.loadingData" /></h1>;
return (
<PageLayout title={`Your ${title} booking is confirmed!`}>
<NamedLink
name="OrderDetailsPage"
params={{ id: orderId }}
activeClassName={css.activeTab}
style={{ marginRight: '2rem' }}
>
Booking details
</NamedLink>
<NamedLink
name="OrderDiscussionPage"
activeClassName={css.activeTab}
params={{ id: orderId }}
>
Discussion
</NamedLink>
<OrderDetailsPanel className={detailsClassName} {...detailsProps} />
<OrderDiscussionPanel className={discussionClassName} />
<PageLayout title={intl.formatMessage({ id: 'OrderPage.title' }, { title })}>
{panel}
</PageLayout>
);
};
const { string, shape, oneOfType, number, oneOf } = PropTypes;
OrderPageComponent.defaultProps = { transaction: null };
OrderPage.propTypes = {
params: shape({ id: oneOfType([number, string]).isRequired }).isRequired,
const { oneOf } = PropTypes;
OrderPageComponent.propTypes = {
intl: intlShape.isRequired,
tab: oneOf(['details', 'discussion']).isRequired,
transaction: propTypes.transaction,
};
const mapStateToProps = state => {
const transactionRef = state.OrderPage.transactionRef;
const transactions = getEntities(state.data, transactionRef ? [transactionRef] : []);
const transaction = transactions.length > 0 ? transactions[0] : null;
return {
transaction,
showOrderError: state.ListingPage.showListingError,
currentUser: state.user.currentUser,
};
};
const OrderPage = connect(mapStateToProps)(injectIntl(OrderPageComponent));
OrderPage.loadData = params => {
return loadData(params);
};
export default OrderPage;

View file

@ -1,10 +1,35 @@
import React from 'react';
import {
createBooking,
createListing,
createTransaction,
createUser,
fakeIntl,
} from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
import OrderPage from './OrderPage';
import { OrderPageComponent } from './OrderPage';
describe('OrderPage', () => {
it('matches snapshot', () => {
const tree = renderShallow(<OrderPage params={{ id: 1234 }} tab="details" />);
const transaction = createTransaction({
id: 'tx-order-1',
state: 'state/preauthorized',
booking: createBooking(
'booking1',
new Date(Date.UTC(2017, 5, 10)),
new Date(Date.UTC(2017, 5, 13))
),
listing: createListing('listing1'),
provider: createUser('provider1'),
});
const props = {
transaction,
tab: 'details',
intl: fakeIntl,
};
const tree = renderShallow(<OrderPageComponent {...props} />);
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,43 +1,63 @@
exports[`OrderPage matches snapshot 1`] = `
<Connect(withRouter(PageLayout))
title="Your Banyan Studios booking is confirmed!">
<withFlattenedRoutes(withRouter(NamedLink))
name="OrderDetailsPage"
params={
Object {
"id": 1234,
}
}
style={
Object {
"marginRight": "2rem",
}
}>
Booking details
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
name="OrderDiscussionPage"
params={
Object {
"id": 1234,
}
}>
Discussion
</withFlattenedRoutes(withRouter(NamedLink))>
title="OrderPage.title">
<OrderDetailsPanel
className="undefined"
confirmationCode="X2587X"
contact={
booking={
Object {
"addressLine1": "350 5th Avenue",
"addressLine2": "New York, NY 10118",
"phoneNumber": "+1 432 43184910",
"attributes": Object {
"end": 2017-06-13T00:00:00.000Z,
"start": 2017-06-10T00:00:00.000Z,
},
"id": UUID {
"uuid": "booking1",
},
"type": "booking",
}
}
imageUrl="http://placehold.it/750x470"
orderId={1234}
title="Banyan Studios" />
<OrderDiscussionPanel
className="" />
className="undefined"
listing={
Object {
"attributes": Object {
"address": "listing1 address",
"description": "listing1 description",
"geolocation": LatLng {
"lat": 40,
"lng": 60,
},
"price": Money {
"amount": 5500,
"currency": "USD",
},
"title": "listing1 title",
},
"author": null,
"id": UUID {
"uuid": "listing1",
},
"images": Array [],
"type": "listing",
}
}
orderState="state/preauthorized"
provider={
Object {
"attributes": Object {
"profile": Object {
"firstName": "provider1 first name",
"lastName": "provider1 last name",
},
},
"id": UUID {
"uuid": "provider1",
},
"type": "user",
}
}
totalPrice={
Money {
"amount": 1000,
"currency": "USD",
}
} />
</Connect(withRouter(PageLayout))>
`;

View file

@ -7,6 +7,7 @@ import CheckoutPage from './CheckoutPage/CheckoutPage.duck';
import EditListingPage from './EditListingPage/EditListingPage.duck';
import InboxPage from './InboxPage/InboxPage.duck';
import ListingPage from './ListingPage/ListingPage.duck';
import OrderPage from './OrderPage/OrderPage.duck';
import SearchPage from './SearchPage/SearchPage.duck';
export { CheckoutPage, EditListingPage, InboxPage, ListingPage, SearchPage };
export { CheckoutPage, EditListingPage, InboxPage, ListingPage, OrderPage, SearchPage };

View file

@ -183,6 +183,7 @@ const routesConfiguration = [
exact: true,
name: 'OrderDetailsPage',
component: props => <OrderPage {...props} tab="details" />,
loadData: params => OrderPage.loadData(params),
},
{
path: '/order/:id/discussion',
@ -190,6 +191,7 @@ const routesConfiguration = [
exact: true,
name: 'OrderDiscussionPage',
component: props => <OrderPage {...props} tab="discussion" />,
loadData: params => OrderPage.loadData(params),
},
],
},

View file

@ -57,6 +57,8 @@
"ModalInMobile.closeModal": "Close modal",
"OrderDetailsPanel.listingTitle": "You have requested to book {title}",
"OrderDetailsPanel.orderStatusMessage": "{providerName} has been notified about the booking request, so sit back and relax",
"OrderPage.loadingData": "Loading order data",
"OrderPage.title": "Order details for ${title}",
"PageLayout.authInfoFailed": "Could not get authentication information.",
"PageLayout.logoutFailed": "Logout failed. Please try again.",
"PaginationLinks.previous": "Previous page",