Merge pull request #548 from sharetribe/render-order-sale-messages

Order page messages
This commit is contained in:
Kimmo Puputti 2017-11-10 11:35:16 +02:00 committed by GitHub
commit 45d4c82942
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 493 additions and 459 deletions

View file

@ -24,6 +24,7 @@
.ownMessage {
/* Preserve newlines, but collapse other whitespace */
white-space: pre-line;
width: 100%;
max-width: 433px;
}

View file

@ -4,6 +4,18 @@
position: relative;
}
.error {
color: var(--failColor);
margin: 13px 0 22px 0;
@media (--viewportMedium) {
margin: 13px 0 23px 0;
}
@media (--viewportLarge) {
margin: 12px 0 23px 0;
}
}
.container {
display: flex;
flex-direction: column;
@ -72,13 +84,13 @@
.orderInfo {
@media (--viewportLarge) {
max-width: 519px;
margin-right: 132px;
max-width: 538px;
margin-right: 108px;
}
}
.heading {
margin: 28px 24px 0 24px;
margin: 27px 24px 0 24px;
@media (--viewportMedium) {
max-width: 80%;
@ -94,18 +106,27 @@
display: block;
}
.messagesContainer {
display: flex;
flex-direction: column;
.orderInfoText {
margin: 18px 24px 0 24px;
@media (--viewportMedium) {
margin: 23px 24px 0 24px;
}
@media (--viewportLarge) {
margin: 23px 0 0 0;
}
}
.message {
display: flex;
margin: 24px 24px 47px 24px;
.infoTextDivider {
display: none;
height: 1px;
background-color: var(--matterColorNegative);
border: none;
margin: 37px 0 0 0;
@media (--viewportLarge) {
margin: 31px 0 0 0;
line-height: 32px;
display: block;
}
}
@ -114,6 +135,11 @@
}
.breakdownMobile {
margin-top: 47px;
@media (--viewportMedium) {
margin-top: 43px;
}
@media (--viewportLarge) {
display: none;
}
@ -176,7 +202,7 @@
margin-bottom: 0;
@media (--viewportLarge) {
margin-top: 0;
margin-top: 1px;
margin-bottom: 0;
}
}
@ -185,25 +211,54 @@
/* Font */
color: var(--matterColorAnti);
margin-top: 5px;
margin-bottom: 14px;
margin-left: 24px;
margin-right: 24px;
margin: 0 24px 0 24px;
@media (--viewportLarge) {
margin-top: 38px;
margin-bottom: 25px;
margin-left: 48px;
margin-right: 48px;
margin: 37px 48px 26px 48px;
}
}
.receipt {
margin-left: 24px;
margin-right: 24px;
.breakdown {
margin: 14px 24px 0 24px;
@media (--viewportMedium) {
margin: 18px 24px 0 24px;
}
@media (--viewportLarge) {
margin-left: 48px;
margin-right: 48px;
margin: 14px 48px 0 48px;
}
}
.messagesContainer {
margin: 46px 24px 0 24px;
@media (--viewportMedium) {
margin: 46px 24px 0 24px;
}
@media (--viewportLarge) {
margin: 43px 0 0 0;
}
}
.messagesContainerWithInfoAbove {
@media (--viewportLarge) {
margin: 38px 0 0 0;
}
}
.messagesHeading {
color: var(--matterColorAnti);
margin: 0;
@media (--viewportMedium) {
margin: 0;
}
}
.messages {
margin-top: 17px;
@media (--viewportMedium) {
margin-top: 19px;
}
}

View file

@ -1,11 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedDate, FormattedMessage } from 'react-intl';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { createSlug } from '../../util/urlHelpers';
import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
import { BookingBreakdown, NamedLink, ResponsiveImage, AvatarMedium } from '../../components';
import {
BookingBreakdown,
NamedLink,
ResponsiveImage,
AvatarMedium,
Messages,
} from '../../components';
import css from './OrderDetailsPanel.css';
@ -14,7 +20,7 @@ const breakdown = transaction => {
return loaded ? (
<BookingBreakdown
className={css.receipt}
className={css.breakdown}
userRole="customer"
transaction={transaction}
booking={transaction.booking}
@ -62,60 +68,26 @@ const orderTitle = (transaction, listingLink, customerName) => {
}
};
const orderMessage = (transaction, listingTitle, providerName) => {
const transitionDate = (
<span className={css.transitionDate}>
<FormattedDate
value={transaction.attributes.lastTransitionedAt}
year="numeric"
month="short"
day="numeric"
/>
</span>
);
const orderMessage = (transaction, providerName) => {
if (propTypes.txIsPreauthorized(transaction)) {
return (
<FormattedMessage id="OrderDetailsPanel.orderPreauthorizedStatus" values={{ providerName }} />
);
} else if (propTypes.txIsAccepted(transaction)) {
return (
<FormattedMessage
id="OrderDetailsPanel.orderAcceptedStatus"
values={{ providerName, transitionDate }}
/>
);
} else if (propTypes.txIsDeclined(transaction)) {
return (
<FormattedMessage
id="OrderDetailsPanel.orderDeclinedStatus"
values={{ providerName, transitionDate }}
/>
);
} else if (propTypes.txIsAutodeclined(transaction)) {
return (
<FormattedMessage
id="OrderDetailsPanel.orderAutoDeclinedStatus"
values={{ providerName, transitionDate }}
/>
);
} else if (propTypes.txIsCanceled(transaction)) {
return (
<FormattedMessage id="OrderDetailsPanel.orderCanceledStatus" values={{ transitionDate }} />
);
} else if (propTypes.txIsDelivered(transaction)) {
return (
<FormattedMessage
id="OrderDetailsPanel.orderDeliveredStatus"
values={{ providerName, transitionDate }}
/>
);
} else {
return null;
}
return null;
};
export const OrderDetailsPanelComponent = props => {
const { rootClassName, className, transaction, intl } = props;
const {
rootClassName,
className,
currentUser,
transaction,
messages,
initialMessageFailed,
fetchMessagesError,
intl,
} = props;
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
const currentProvider = ensureUser(currentTransaction.provider);
@ -160,46 +132,72 @@ export const OrderDetailsPanelComponent = props => {
const bookingInfo = breakdown(currentTransaction);
const orderHeading = orderTitle(currentTransaction, listingLink, customerDisplayName);
const message = listingDeleted
const orderInfoText = listingDeleted
? orderMessageDeletedListing
: orderMessage(currentTransaction, listingLink, authorDisplayName);
: orderMessage(currentTransaction, authorDisplayName);
const showInfoMessage = !!orderInfoText;
const firstImage =
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
const messagesContainerClasses = classNames(css.messagesContainer, {
[css.messagesContainerWithInfoAbove]: showInfoMessage,
});
const showMessages = messages.length > 0 || initialMessageFailed || fetchMessagesError;
const messagesContainer = showMessages ? (
<div className={messagesContainerClasses}>
<h3 className={css.messagesHeading}>
<FormattedMessage id="OrderDetailsPanel.messagesHeading" />
</h3>
{initialMessageFailed ? (
<p className={css.error}>
<FormattedMessage id="OrderDetailsPanel.initialMessageFailed" />
</p>
) : null}
{fetchMessagesError ? (
<p className={css.error}>
<FormattedMessage id="OrderDetailsPanel.messageLoadingFailed" />
</p>
) : null}
<Messages className={css.messages} messages={messages} currentUser={currentUser} />
</div>
) : null;
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<div className={css.container}>
<div className={css.imageWrapperMobile}>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={firstImage}
nameSet={[
{ name: 'landscape-crop', size: '400w' },
{ name: 'landscape-crop2x', size: '800w' },
]}
sizes="100vw"
/>
</div>
</div>
<div className={classNames(css.avatarWrapper, css.avatarMobile)}>
<AvatarMedium user={currentProvider} />
</div>
<div className={css.orderInfo}>
<div className={css.imageWrapperMobile}>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={firstImage}
nameSet={[
{ name: 'landscape-crop', size: '400w' },
{ name: 'landscape-crop2x', size: '800w' },
]}
sizes="100vw"
/>
</div>
</div>
<div className={classNames(css.avatarWrapper, css.avatarMobile)}>
<AvatarMedium user={currentProvider} />
</div>
<h1 className={css.heading}>{orderHeading}</h1>
<div className={css.message}>{message}</div>
</div>
<div className={css.bookingBreakdownContainer}>
{showInfoMessage ? <p className={css.orderInfoText}>{orderInfoText}</p> : null}
{showInfoMessage ? <hr className={css.infoTextDivider} /> : null}
<div className={css.breakdownMobile}>
<h3 className={css.bookingBreakdownTitle}>
<FormattedMessage id="OrderDetailsPanel.bookingBreakdownTitle" />
</h3>
{bookingInfo}
</div>
{messagesContainer}
</div>
<div className={css.bookingBreakdownContainer}>
<div className={css.breakdownDesktop}>
<div className={css.breakdownImageWrapper}>
<div className={css.aspectWrapper}>
@ -241,14 +239,20 @@ export const OrderDetailsPanelComponent = props => {
OrderDetailsPanelComponent.defaultProps = {
rootClassName: null,
className: null,
fetchMessagesError: null,
};
const { string } = PropTypes;
const { string, arrayOf, bool } = PropTypes;
OrderDetailsPanelComponent.propTypes = {
rootClassName: string,
className: string,
currentUser: propTypes.currentUser,
transaction: propTypes.transaction.isRequired,
messages: arrayOf(propTypes.message).isRequired,
initialMessageFailed: bool.isRequired,
fetchMessagesError: propTypes.error,
// from injectIntl
intl: intlShape,

View file

@ -58,39 +58,45 @@ const txDelivered = createTransaction({
});
describe('OrderDetailsPanel', () => {
const panelBaseProps = {
intl: fakeIntl,
messages: [],
initialMessageFailed: false,
};
it('preauthorized matches snapshot', () => {
const tree = renderShallow(
<OrderDetailsPanelComponent transaction={txPreauthorized} intl={fakeIntl} />
<OrderDetailsPanelComponent transaction={txPreauthorized} {...panelBaseProps} />
);
expect(tree).toMatchSnapshot();
});
it('accepted matches snapshot', () => {
const tree = renderShallow(
<OrderDetailsPanelComponent transaction={txAccepted} intl={fakeIntl} />
<OrderDetailsPanelComponent transaction={txAccepted} {...panelBaseProps} />
);
expect(tree).toMatchSnapshot();
});
it('declined matches snapshot', () => {
const tree = renderShallow(
<OrderDetailsPanelComponent transaction={txDeclined} intl={fakeIntl} />
<OrderDetailsPanelComponent transaction={txDeclined} {...panelBaseProps} />
);
expect(tree).toMatchSnapshot();
});
it('autodeclined matches snapshot', () => {
const tree = renderShallow(
<OrderDetailsPanelComponent transaction={txAutoDeclined} intl={fakeIntl} />
<OrderDetailsPanelComponent transaction={txAutoDeclined} {...panelBaseProps} />
);
expect(tree).toMatchSnapshot();
});
it('canceled matches snapshot', () => {
const tree = renderShallow(
<OrderDetailsPanelComponent transaction={txCanceled} intl={fakeIntl} />
<OrderDetailsPanelComponent transaction={txCanceled} {...panelBaseProps} />
);
expect(tree).toMatchSnapshot();
});
it('delivered matches snapshot', () => {
const tree = renderShallow(
<OrderDetailsPanelComponent transaction={txDelivered} intl={fakeIntl} />
<OrderDetailsPanelComponent transaction={txDelivered} {...panelBaseProps} />
);
expect(tree).toMatchSnapshot();
});
@ -108,7 +114,7 @@ describe('OrderDetailsPanel', () => {
provider: createUser('provider'),
customer: createUser('customer'),
});
const panel = shallow(<OrderDetailsPanelComponent transaction={tx} intl={fakeIntl} />);
const panel = shallow(<OrderDetailsPanelComponent transaction={tx} {...panelBaseProps} />);
const breakdownProps = panel.find(BookingBreakdown).props();
expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD'));
});

View file

@ -7,50 +7,50 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
<div>
<div>
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
},
Object {
"name": "landscape-crop2x",
"size": "800w",
},
]
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
Object {
"name": "landscape-crop2x",
"size": "800w",
"id": UUID {
"uuid": "provider",
},
]
"type": "user",
}
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
"id": UUID {
"uuid": "provider",
},
"type": "user",
}
}
/>
</div>
<div>
<h1>
<span>
<span>
@ -83,26 +83,6 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
/>
</span>
</h1>
<div>
<FormattedMessage
id="OrderDetailsPanel.orderAcceptedStatus"
values={
Object {
"providerName": "provider display name",
"transitionDate": <span>
<FormattedDate
day="numeric"
month="short"
value={2017-06-01T00:00:00.000Z}
year="numeric"
/>
</span>,
}
}
/>
</div>
</div>
<div>
<div>
<h3>
<FormattedMessage
@ -231,6 +211,8 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
userRole="customer"
/>
</div>
</div>
<div>
<div>
<div>
<div>
@ -429,50 +411,50 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
<div>
<div>
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
},
Object {
"name": "landscape-crop2x",
"size": "800w",
},
]
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
Object {
"name": "landscape-crop2x",
"size": "800w",
"id": UUID {
"uuid": "provider",
},
]
"type": "user",
}
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
"id": UUID {
"uuid": "provider",
},
"type": "user",
}
}
/>
</div>
<div>
<h1>
<FormattedMessage
id="OrderDetailsPanel.orderAutoDeclinedTitle"
@ -493,26 +475,6 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
}
/>
</h1>
<div>
<FormattedMessage
id="OrderDetailsPanel.orderAutoDeclinedStatus"
values={
Object {
"providerName": "provider display name",
"transitionDate": <span>
<FormattedDate
day="numeric"
month="short"
value={2017-06-01T00:00:00.000Z}
year="numeric"
/>
</span>,
}
}
/>
</div>
</div>
<div>
<div>
<h3>
<FormattedMessage
@ -641,6 +603,8 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
userRole="customer"
/>
</div>
</div>
<div>
<div>
<div>
<div>
@ -839,50 +803,50 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
<div>
<div>
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
},
Object {
"name": "landscape-crop2x",
"size": "800w",
},
]
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
Object {
"name": "landscape-crop2x",
"size": "800w",
"id": UUID {
"uuid": "provider",
},
]
"type": "user",
}
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
"id": UUID {
"uuid": "provider",
},
"type": "user",
}
}
/>
</div>
<div>
<h1>
<span>
<span>
@ -915,7 +879,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
/>
</span>
</h1>
<div>
<p>
<FormattedMessage
id="OrderDetailsPanel.orderPreauthorizedStatus"
values={
@ -924,9 +888,8 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
}
}
/>
</div>
</div>
<div>
</p>
<hr />
<div>
<h3>
<FormattedMessage
@ -1055,6 +1018,8 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
userRole="customer"
/>
</div>
</div>
<div>
<div>
<div>
<div>
@ -1253,50 +1218,50 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = `
<div>
<div>
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
},
Object {
"name": "landscape-crop2x",
"size": "800w",
},
]
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
Object {
"name": "landscape-crop2x",
"size": "800w",
"id": UUID {
"uuid": "provider",
},
]
"type": "user",
}
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
"id": UUID {
"uuid": "provider",
},
"type": "user",
}
}
/>
</div>
<div>
<h1>
<FormattedMessage
id="OrderDetailsPanel.orderDeclinedTitle"
@ -1317,26 +1282,6 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = `
}
/>
</h1>
<div>
<FormattedMessage
id="OrderDetailsPanel.orderDeclinedStatus"
values={
Object {
"providerName": "provider display name",
"transitionDate": <span>
<FormattedDate
day="numeric"
month="short"
value={2017-06-01T00:00:00.000Z}
year="numeric"
/>
</span>,
}
}
/>
</div>
</div>
<div>
<div>
<h3>
<FormattedMessage
@ -1465,6 +1410,8 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = `
userRole="customer"
/>
</div>
</div>
<div>
<div>
<div>
<div>
@ -1663,50 +1610,50 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
<div>
<div>
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
},
Object {
"name": "landscape-crop2x",
"size": "800w",
},
]
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
Object {
"name": "landscape-crop2x",
"size": "800w",
"id": UUID {
"uuid": "provider",
},
]
"type": "user",
}
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
"id": UUID {
"uuid": "provider",
},
"type": "user",
}
}
/>
</div>
<div>
<h1>
<FormattedMessage
id="OrderDetailsPanel.orderDeliveredTitle"
@ -1727,26 +1674,6 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
}
/>
</h1>
<div>
<FormattedMessage
id="OrderDetailsPanel.orderDeliveredStatus"
values={
Object {
"providerName": "provider display name",
"transitionDate": <span>
<FormattedDate
day="numeric"
month="short"
value={2017-06-01T00:00:00.000Z}
year="numeric"
/>
</span>,
}
}
/>
</div>
</div>
<div>
<div>
<h3>
<FormattedMessage
@ -1875,6 +1802,8 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
userRole="customer"
/>
</div>
</div>
<div>
<div>
<div>
<div>
@ -2073,50 +2002,50 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
<div>
<div>
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
},
Object {
"name": "landscape-crop2x",
"size": "800w",
},
]
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
Object {
"name": "landscape-crop2x",
"size": "800w",
"id": UUID {
"uuid": "provider",
},
]
"type": "user",
}
}
noImageMessage={null}
rootClassName={null}
sizes="100vw"
/>
</div>
</div>
<div
className=""
>
<AvatarMedium
user={
Object {
"attributes": Object {
"banned": false,
"profile": Object {
"abbreviatedName": "TT",
"displayName": "provider display name",
},
},
"id": UUID {
"uuid": "provider",
},
"type": "user",
}
}
/>
</div>
<div>
<h1>
<span>
<span>
@ -2149,7 +2078,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
/>
</span>
</h1>
<div>
<p>
<FormattedMessage
id="OrderDetailsPanel.orderPreauthorizedStatus"
values={
@ -2158,9 +2087,8 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
}
}
/>
</div>
</div>
<div>
</p>
<hr />
<div>
<h3>
<FormattedMessage
@ -2289,6 +2217,8 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
userRole="customer"
/>
</div>
</div>
<div>
<div>
<div>
<div>

View file

@ -118,12 +118,15 @@ export const fetchMessages = txId => (dispatch, getState, sdk) => {
dispatch(fetchMessagesRequest());
return sdk.messages
.query({ transaction_id: txId })
.query({ transaction_id: txId, include: ['sender', 'sender.profileImage'] })
.then(response => {
const entities = updatedEntities({}, response.data);
const messageIds = response.data.data.map(d => d.id);
const denormalized = denormalisedEntities(entities, 'message', messageIds);
// Messages come latest first, so we need to reverse the order
denormalized.reverse();
dispatch(fetchMessagesSuccess(denormalized));
})
.catch(e => {

View file

@ -29,6 +29,8 @@ export const OrderPageComponent = props => {
const {
currentUser,
fetchOrderError,
fetchMessagesError,
messages,
messageSendingFailedToTransaction,
intl,
params,
@ -39,13 +41,11 @@ export const OrderPageComponent = props => {
const currentListing = ensureListing(currentTransaction.listing);
const listingTitle = currentListing.attributes.title;
if (messageSendingFailedToTransaction) {
// TODO: render error message with other messages
console.error(
'failed to send initial message to transaction:',
messageSendingFailedToTransaction
);
}
const initialMessageFailed = !!(
messageSendingFailedToTransaction &&
currentTransaction.id &&
messageSendingFailedToTransaction.uuid === currentTransaction.id.uuid
);
// Redirect users with someone else's direct link to their own inbox/orders page.
const isDataAvailable =
@ -77,7 +77,14 @@ export const OrderPageComponent = props => {
const panel =
isDataAvailable && currentTransaction.id ? (
<OrderDetailsPanel className={detailsClassName} transaction={currentTransaction} />
<OrderDetailsPanel
className={detailsClassName}
currentUser={currentUser}
transaction={currentTransaction}
messages={messages}
initialMessageFailed={initialMessageFailed}
fetchMessagesError={fetchMessagesError}
/>
) : (
loadingOrFaildFetching
);
@ -103,31 +110,46 @@ export const OrderPageComponent = props => {
OrderPageComponent.defaultProps = {
currentUser: null,
fetchOrderError: null,
fetchMessagesError: null,
messageSendingFailedToTransaction: null,
transaction: null,
};
const { bool, oneOf, shape, string } = PropTypes;
const { bool, oneOf, shape, string, array } = PropTypes;
OrderPageComponent.propTypes = {
params: shape({ id: string }).isRequired,
tab: oneOf(['details', 'discussion']).isRequired,
currentUser: propTypes.currentUser,
fetchOrderError: propTypes.error,
fetchMessagesError: propTypes.error,
messages: array.isRequired,
messageSendingFailedToTransaction: propTypes.uuid,
intl: intlShape.isRequired,
params: shape({ id: string }).isRequired,
scrollingDisabled: bool.isRequired,
tab: oneOf(['details', 'discussion']).isRequired,
transaction: propTypes.transaction,
// from injectIntl
intl: intlShape.isRequired,
};
const mapStateToProps = state => {
const { fetchOrderError, transactionRef, messageSendingFailedToTransaction } = state.OrderPage;
const { currentUser } = state.user;
const {
fetchOrderError,
transactionRef,
fetchMessagesError,
messages,
messageSendingFailedToTransaction,
} = state.OrderPage;
const transactions = getMarketplaceEntities(state, transactionRef ? [transactionRef] : []);
const transaction = transactions.length > 0 ? transactions[0] : null;
return {
currentUser,
fetchOrderError,
fetchMessagesError,
messages,
messageSendingFailedToTransaction,
scrollingDisabled: isScrollingDisabled(state),
transaction,

View file

@ -29,23 +29,15 @@ describe('OrderPage', () => {
});
const props = {
authInProgress: false,
currentUserHasListings: false,
isAuthenticated: false,
onLogout: noop,
onManageDisableScrolling: noop,
currentUser: createCurrentUser('customer1'),
params: { id: txId },
transaction,
tab: 'details',
intl: fakeIntl,
currentUser: createCurrentUser('customer1'),
messages: [],
scrollingDisabled: false,
location: { search: '' },
history: {
push: () => console.log('HistoryPush called'),
},
sendVerificationEmailInProgress: false,
onResendVerificationEmail: noop,
transaction,
intl: fakeIntl,
};
const tree = renderShallow(<OrderPageComponent {...props} />);

View file

@ -21,6 +21,29 @@ exports[`OrderPage matches snapshot 1`] = `
>
<InjectIntl(Component)
className="undefined"
currentUser={
Object {
"attributes": Object {
"banned": false,
"email": "customer1@example.com",
"emailVerified": true,
"profile": Object {
"abbreviatedName": "customer1 abbreviated name",
"displayName": "customer1 display name",
"firstName": "customer1 first name",
"lastName": "customer1 last name",
},
"stripeConnected": true,
},
"id": UUID {
"uuid": "customer1",
},
"type": "currentUser",
}
}
fetchMessagesError={null}
initialMessageFailed={false}
messages={Array []}
transaction={
Object {
"attributes": Object {

View file

@ -259,17 +259,15 @@
"OrderDetailsPanel.deletedListingOrderTitle": "a listing",
"OrderDetailsPanel.deletedListingTitle": "Deleted listing",
"OrderDetailsPanel.hostedBy": "Hosted by {name}",
"OrderDetailsPanel.initialMessageFailed": "Whoops, failed to send message from checkout.",
"OrderDetailsPanel.messageDeletedListing": "However, the listing is deleted and cannot be viewed anymore.",
"OrderDetailsPanel.orderAcceptedStatus": "{providerName} accepted the request on {transitionDate}.",
"OrderDetailsPanel.messageLoadingFailed": "Something went wrong when loading messages. Please refresh the page and try again.",
"OrderDetailsPanel.messagesHeading": "Activity",
"OrderDetailsPanel.orderAcceptedSubtitle": "You have booked {listingLink}",
"OrderDetailsPanel.orderAcceptedTitle": "Woohoo {customerName}!",
"OrderDetailsPanel.orderAutoDeclinedStatus": "The request expired on {transitionDate}. {providerName} never replied to the request.",
"OrderDetailsPanel.orderAutoDeclinedTitle": "You requested to book {listingLink}",
"OrderDetailsPanel.orderCanceledStatus": "The request was cancelled on {transitionDate}",
"OrderDetailsPanel.orderCanceledTitle": "You booked {listingLink}",
"OrderDetailsPanel.orderDeclinedStatus": "Unfortunately {providerName} declined the request on {transitionDate}.",
"OrderDetailsPanel.orderDeclinedTitle": "You requested to book {listingLink}",
"OrderDetailsPanel.orderDeliveredStatus": " ",
"OrderDetailsPanel.orderDeliveredTitle": "Booking period has ended",
"OrderDetailsPanel.orderPreauthorizedStatus": "{providerName} has been notified about the booking request, so sit back and relax.",
"OrderDetailsPanel.orderPreauthorizedSubtitle": "You have requested to book {listingLink}",