mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-27 19:42:11 +10:00
Merge pull request #598 from sharetribe/transactionpanel
Transactionpanel
This commit is contained in:
commit
241fca0f1e
19 changed files with 10897 additions and 9277 deletions
|
|
@ -5,7 +5,7 @@ import { dropWhile } from 'lodash';
|
|||
import classNames from 'classnames';
|
||||
import { Avatar, InlineTextButton, ReviewRating } from '../../components';
|
||||
import { formatDate } from '../../util/dates';
|
||||
import { ensureTransaction, ensureUser, ensureListing } from '../../util/data';
|
||||
import { ensureTransaction, ensureUser, ensureListing, userDisplayName } from '../../util/data';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import * as log from '../../util/log';
|
||||
|
||||
|
|
@ -212,7 +212,13 @@ const Transition = props => {
|
|||
const currentTransaction = ensureTransaction(transaction);
|
||||
const customer = currentTransaction.customer;
|
||||
const provider = currentTransaction.provider;
|
||||
const listingTitle = currentTransaction.listing.attributes.title;
|
||||
|
||||
const deletedListing = intl.formatMessage({
|
||||
id: 'ActivityFeed.deletedListing',
|
||||
});
|
||||
const listingTitle = currentTransaction.listing.attributes.deleted
|
||||
? deletedListing
|
||||
: currentTransaction.listing.attributes.title;
|
||||
const lastTransition = currentTransaction.attributes.lastTransition;
|
||||
|
||||
const ownRole =
|
||||
|
|
@ -220,10 +226,13 @@ const Transition = props => {
|
|||
? propTypes.TX_TRANSITION_ACTOR_CUSTOMER
|
||||
: propTypes.TX_TRANSITION_ACTOR_PROVIDER;
|
||||
|
||||
const bannedUserDisplayName = intl.formatMessage({
|
||||
id: 'ActivityFeed.bannedUserDisplayName',
|
||||
});
|
||||
const otherUsersName =
|
||||
ownRole === propTypes.TX_TRANSITION_ACTOR_CUSTOMER
|
||||
? provider.attributes.profile.displayName
|
||||
: customer.attributes.profile.displayName;
|
||||
? userDisplayName(provider, bannedUserDisplayName)
|
||||
: userDisplayName(customer, bannedUserDisplayName);
|
||||
|
||||
const transitionMessage = resolveTransitionMessage(
|
||||
transition,
|
||||
|
|
|
|||
|
|
@ -1,288 +0,0 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
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;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
max-width: 1156px; /* 1060 + (paddingLeft + paddingRight) */
|
||||
margin: 0 auto 57px auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.imageWrapperMobile {
|
||||
/* Layout */
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Firefox doesn't support image aspect ratio inside flexbox */
|
||||
.aspectWrapper {
|
||||
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
|
||||
background-color: var(--matterColorNegative); /* Loading BG color */
|
||||
}
|
||||
|
||||
.rootForImage {
|
||||
/* Layout - image will take space defined by aspect ratio wrapper */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarWrapper {
|
||||
/* Position (over the listing image)*/
|
||||
margin-left: 24px;
|
||||
margin-top: -31px;
|
||||
|
||||
/* Rendering context to the same lavel as listing image */
|
||||
position: relative;
|
||||
|
||||
/* Layout */
|
||||
display: block;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-left: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarMobile {
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.orderInfo {
|
||||
margin-bottom: 210px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
max-width: 538px;
|
||||
margin-right: 108px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 29px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 25px 24px 0 24px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
max-width: 100%;
|
||||
margin: 177px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mainTitle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.orderInfoText {
|
||||
margin: 18px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 23px 24px 0 24px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 23px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.infoTextDivider {
|
||||
display: none;
|
||||
|
||||
height: 1px;
|
||||
background-color: var(--matterColorNegative);
|
||||
border: none;
|
||||
margin: 37px 0 0 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.transitionDate {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.breakdownMobile {
|
||||
margin-top: 47px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 43px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownDesktop {
|
||||
display: none;
|
||||
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
width: 409px;
|
||||
padding-bottom: 55px;
|
||||
background-color: var(--matterColorLight);
|
||||
border: 1px solid var(--matterColorNegative);
|
||||
border-radius: 2px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownImageWrapper {
|
||||
/* Layout */
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bookingBreakdownContainer {
|
||||
margin: 1px 0 0 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 123px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownHeadings {
|
||||
display: none;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
margin-left: 48px;
|
||||
margin-right: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownTitle {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownSubtitle {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 1px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bookingBreakdownTitle {
|
||||
/* Font */
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
margin: 0 24px 0 24px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin: 37px 48px 26px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdown {
|
||||
margin: 14px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 18px 24px 0 24px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 14px 48px 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.feedContainer {
|
||||
margin: 46px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 46px 24px 0 24px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 43px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feedContainerWithInfoAbove {
|
||||
@media (--viewportLarge) {
|
||||
margin: 38px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feedHeading {
|
||||
color: var(--matterColorAnti);
|
||||
margin: 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feed {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sendMessageForm {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-shadow: var(--boxShadowBottomForm);
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 46px;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sendMessageFormFocusedInMobileSafari {
|
||||
position: absolute;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,439 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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 { isMobileSafari } from '../../util/userAgent';
|
||||
import {
|
||||
ActivityFeed,
|
||||
AvatarMedium,
|
||||
BookingBreakdown,
|
||||
NamedLink,
|
||||
ResponsiveImage,
|
||||
ReviewModal,
|
||||
} from '../../components';
|
||||
import { SendMessageForm } from '../../containers';
|
||||
|
||||
import css from './OrderDetailsPanel.css';
|
||||
|
||||
const breakdown = transaction => {
|
||||
const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
|
||||
|
||||
return loaded ? (
|
||||
<BookingBreakdown
|
||||
className={css.breakdown}
|
||||
userRole="customer"
|
||||
transaction={transaction}
|
||||
booking={transaction.booking}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const orderTitle = (transaction, listingLink, customerName) => {
|
||||
if (propTypes.txIsPreauthorized(transaction)) {
|
||||
return (
|
||||
<span>
|
||||
<span className={css.mainTitle}>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.orderPreauthorizedTitle"
|
||||
values={{ customerName }}
|
||||
/>
|
||||
</span>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.orderPreauthorizedSubtitle"
|
||||
values={{ listingLink }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (propTypes.txIsAccepted(transaction)) {
|
||||
return (
|
||||
<span>
|
||||
<span className={css.mainTitle}>
|
||||
<FormattedMessage id="OrderDetailsPanel.orderAcceptedTitle" values={{ customerName }} />
|
||||
</span>
|
||||
<FormattedMessage id="OrderDetailsPanel.orderAcceptedSubtitle" values={{ listingLink }} />
|
||||
</span>
|
||||
);
|
||||
} else if (propTypes.txIsDeclined(transaction)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.orderDeclinedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else if (propTypes.txIsAutodeclined(transaction)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.orderDeclinedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else if (propTypes.txIsCanceled(transaction)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.orderCancelledTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
propTypes.txIsDelivered(transaction) ||
|
||||
propTypes.txHasFirstReview(transaction) ||
|
||||
propTypes.txIsReviewed(transaction)
|
||||
) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.orderDeliveredTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const orderMessage = (transaction, providerName) => {
|
||||
if (propTypes.txIsPreauthorized(transaction)) {
|
||||
return (
|
||||
<FormattedMessage id="OrderDetailsPanel.orderPreauthorizedInfo" values={{ providerName }} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export class OrderDetailsPanelComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sendMessageFormFocused: false,
|
||||
isReviewModalOpen: false,
|
||||
reviewSubmitted: false,
|
||||
};
|
||||
this.onOpenReviewModal = this.onOpenReviewModal.bind(this);
|
||||
this.onSubmitReview = this.onSubmitReview.bind(this);
|
||||
}
|
||||
|
||||
onOpenReviewModal() {
|
||||
this.setState({ isReviewModalOpen: true });
|
||||
}
|
||||
|
||||
onSubmitReview(values) {
|
||||
const { onSendReview, transaction } = this.props;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const { reviewRating, reviewContent } = values;
|
||||
const rating = Number.parseInt(reviewRating, 10);
|
||||
onSendReview(currentTransaction, rating, reviewContent)
|
||||
.then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true }))
|
||||
.catch(e => {
|
||||
// Do nothing.
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
currentUser,
|
||||
transaction,
|
||||
totalMessagePages,
|
||||
oldestMessagePageFetched,
|
||||
messages,
|
||||
initialMessageFailed,
|
||||
fetchMessagesInProgress,
|
||||
fetchMessagesError,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
sendReviewInProgress,
|
||||
sendReviewError,
|
||||
onManageDisableScrolling,
|
||||
onShowMoreMessages,
|
||||
onSendMessage,
|
||||
onResetForm,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const currentProvider = ensureUser(currentTransaction.provider);
|
||||
const currentCustomer = ensureUser(currentTransaction.customer);
|
||||
|
||||
const listingLoaded = !!currentListing.id;
|
||||
const listingDeleted = listingLoaded && currentListing.attributes.deleted;
|
||||
|
||||
const bannedUserDisplayName = intl.formatMessage({
|
||||
id: 'OrderDetailsPanel.bannedUserDisplayName',
|
||||
});
|
||||
const deletedListingTitle = intl.formatMessage({
|
||||
id: 'OrderDetailsPanel.deletedListingTitle',
|
||||
});
|
||||
const deletedListingOrderTitle = intl.formatMessage({
|
||||
id: 'OrderDetailsPanel.deletedListingOrderTitle',
|
||||
});
|
||||
const orderMessageDeletedListing = intl.formatMessage({
|
||||
id: 'OrderDetailsPanel.messageDeletedListing',
|
||||
});
|
||||
|
||||
const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
|
||||
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
|
||||
|
||||
let otherUserDisplayName = '';
|
||||
const currentUserIsCustomer =
|
||||
currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
|
||||
const currentUserIsProvider =
|
||||
currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
|
||||
|
||||
if (currentUserIsCustomer) {
|
||||
otherUserDisplayName = authorDisplayName;
|
||||
} else if (currentUserIsProvider) {
|
||||
otherUserDisplayName = customerDisplayName;
|
||||
}
|
||||
|
||||
let listingLink = null;
|
||||
|
||||
if (listingLoaded && currentListing.attributes.title) {
|
||||
const title = currentListing.attributes.title;
|
||||
const params = { id: currentListing.id.uuid, slug: createSlug(title) };
|
||||
listingLink = (
|
||||
<NamedLink name="ListingPage" params={params}>
|
||||
{title}
|
||||
</NamedLink>
|
||||
);
|
||||
} else {
|
||||
listingLink = deletedListingOrderTitle;
|
||||
}
|
||||
|
||||
const listingTitle = currentListing.attributes.deleted
|
||||
? deletedListingTitle
|
||||
: currentListing.attributes.title;
|
||||
|
||||
const bookingInfo = breakdown(currentTransaction);
|
||||
const orderHeading = orderTitle(currentTransaction, listingLink, customerDisplayName);
|
||||
const orderInfoText = listingDeleted
|
||||
? orderMessageDeletedListing
|
||||
: orderMessage(currentTransaction, authorDisplayName);
|
||||
const showInfoMessage = !!orderInfoText;
|
||||
|
||||
const firstImage =
|
||||
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
|
||||
|
||||
const feedContainerClasses = classNames(css.feedContainer, {
|
||||
[css.feedContainerWithInfoAbove]: showInfoMessage,
|
||||
});
|
||||
const txTransitions = currentTransaction.attributes.transitions
|
||||
? currentTransaction.attributes.transitions
|
||||
: [];
|
||||
const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
|
||||
const handleShowOlderMessages = () => {
|
||||
onShowMoreMessages(currentTransaction.id);
|
||||
};
|
||||
const showFeed =
|
||||
messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError;
|
||||
const feedContainer = showFeed ? (
|
||||
<div className={feedContainerClasses}>
|
||||
<h3 className={css.feedHeading}>
|
||||
<FormattedMessage id="OrderDetailsPanel.activityHeading" />
|
||||
</h3>
|
||||
{initialMessageFailed ? (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="OrderDetailsPanel.initialMessageFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
{fetchMessagesError ? (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="OrderDetailsPanel.messageLoadingFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
<ActivityFeed
|
||||
className={css.feed}
|
||||
messages={messages}
|
||||
transaction={currentTransaction}
|
||||
currentUser={currentUser}
|
||||
hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress}
|
||||
onOpenReviewModal={this.onOpenReviewModal}
|
||||
onShowOlderMessages={handleShowOlderMessages}
|
||||
fetchMessagesInProgress={fetchMessagesInProgress}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const sendMessagePlaceholder = intl.formatMessage(
|
||||
{ id: 'OrderDetailsPanel.sendMessagePlaceholder' },
|
||||
{ name: otherUserDisplayName }
|
||||
);
|
||||
|
||||
const sendMessageFormName = 'OrderDetailsPanel.SendMessageForm';
|
||||
const scrollToMessage = messageId => {
|
||||
const selector = `#msg-${messageId.uuid}`;
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
el.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
const isMobSaf = isMobileSafari();
|
||||
const handleSendMessageFormFocus = () => {
|
||||
this.setState({ sendMessageFormFocused: true });
|
||||
if (isMobSaf) {
|
||||
// Scroll to bottom
|
||||
window.scroll({ top: document.body.scrollHeight, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
const handleSendMessageFormBlur = () => {
|
||||
this.setState({ sendMessageFormFocused: false });
|
||||
};
|
||||
const handleMessageSubmit = values => {
|
||||
const message = values.message ? values.message.trim() : null;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
onSendMessage(currentTransaction.id, message)
|
||||
.then(messageId => {
|
||||
onResetForm(sendMessageFormName);
|
||||
scrollToMessage(messageId);
|
||||
})
|
||||
.catch(e => {
|
||||
// Ignore, Redux handles the error
|
||||
});
|
||||
};
|
||||
const sendMessageFormClasses = classNames(css.sendMessageForm, {
|
||||
[css.sendMessageFormFocusedInMobileSafari]: isMobSaf && this.state.sendMessageFormFocused,
|
||||
});
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.container}>
|
||||
<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>
|
||||
{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>
|
||||
{feedContainer}
|
||||
<SendMessageForm
|
||||
form={sendMessageFormName}
|
||||
rootClassName={sendMessageFormClasses}
|
||||
messagePlaceholder={sendMessagePlaceholder}
|
||||
inProgress={sendMessageInProgress}
|
||||
sendMessageError={sendMessageError}
|
||||
onFocus={handleSendMessageFormFocus}
|
||||
onBlur={handleSendMessageFormBlur}
|
||||
onSubmit={handleMessageSubmit}
|
||||
/>
|
||||
</div>
|
||||
<div className={css.bookingBreakdownContainer}>
|
||||
<div className={css.breakdownDesktop}>
|
||||
<div className={css.breakdownImageWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
rootClassName={css.rootForImage}
|
||||
alt={listingTitle}
|
||||
image={firstImage}
|
||||
nameSet={[
|
||||
{ name: 'landscape-crop', size: '400w' },
|
||||
{ name: 'landscape-crop2x', size: '800w' },
|
||||
]}
|
||||
sizes="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.avatarWrapper}>
|
||||
<AvatarMedium user={currentProvider} />
|
||||
</div>
|
||||
<div className={css.breakdownHeadings}>
|
||||
<h2 className={css.breakdownTitle}>{listingTitle}</h2>
|
||||
<p className={css.breakdownSubtitle}>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.hostedBy"
|
||||
values={{ name: authorDisplayName }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<h3 className={css.bookingBreakdownTitle}>
|
||||
<FormattedMessage id="OrderDetailsPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
{bookingInfo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReviewModal
|
||||
id="ReviewOrderModal"
|
||||
isOpen={this.state.isReviewModalOpen}
|
||||
onCloseModal={() => this.setState({ isReviewModalOpen: false })}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
onSubmitReview={this.onSubmitReview}
|
||||
revieweeName={authorDisplayName}
|
||||
reviewSent={this.state.reviewSubmitted}
|
||||
sendReviewInProgress={sendReviewInProgress}
|
||||
sendReviewError={sendReviewError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrderDetailsPanelComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
currentUser: null,
|
||||
fetchMessagesError: null,
|
||||
sendMessageError: null,
|
||||
sendReviewError: null,
|
||||
};
|
||||
|
||||
const { string, arrayOf, bool, func, number } = PropTypes;
|
||||
|
||||
OrderDetailsPanelComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
|
||||
currentUser: propTypes.currentUser,
|
||||
transaction: propTypes.transaction.isRequired,
|
||||
totalMessagePages: number.isRequired,
|
||||
oldestMessagePageFetched: number.isRequired,
|
||||
messages: arrayOf(propTypes.message).isRequired,
|
||||
initialMessageFailed: bool.isRequired,
|
||||
fetchMessagesInProgress: bool.isRequired,
|
||||
fetchMessagesError: propTypes.error,
|
||||
sendMessageInProgress: bool.isRequired,
|
||||
sendMessageError: propTypes.error,
|
||||
sendReviewInProgress: bool.isRequired,
|
||||
sendReviewError: propTypes.error,
|
||||
onManageDisableScrolling: func.isRequired,
|
||||
onShowMoreMessages: func.isRequired,
|
||||
onSendMessage: func.isRequired,
|
||||
onSendReview: func.isRequired,
|
||||
onResetForm: func.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
const OrderDetailsPanel = injectIntl(OrderDetailsPanelComponent);
|
||||
|
||||
export default OrderDetailsPanel;
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import {
|
||||
createBooking,
|
||||
createListing,
|
||||
createUser,
|
||||
createTransaction,
|
||||
createCurrentUser,
|
||||
createMessage,
|
||||
} from '../../util/test-data';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { BookingBreakdown } from '../../components';
|
||||
import { OrderDetailsPanelComponent } from './OrderDetailsPanel.js';
|
||||
|
||||
const { Money } = types;
|
||||
|
||||
const baseTxAttrs = {
|
||||
total: new Money(16500, 'USD'),
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
provider: createUser('provider'),
|
||||
customer: createUser('customer'),
|
||||
};
|
||||
|
||||
const txPreauthorized = createTransaction({
|
||||
id: 'order-preauthorized',
|
||||
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txAccepted = createTransaction({
|
||||
id: 'order-accepted',
|
||||
lastTransition: propTypes.TX_TRANSITION_ACCEPT,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txDeclined = createTransaction({
|
||||
id: 'order-declined',
|
||||
lastTransition: propTypes.TX_TRANSITION_DECLINE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txAutoDeclined = createTransaction({
|
||||
id: 'order-autodeclined',
|
||||
lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txCanceled = createTransaction({
|
||||
id: 'order-canceled',
|
||||
lastTransition: propTypes.TX_TRANSITION_CANCELED,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txDelivered = createTransaction({
|
||||
id: 'order-delivered',
|
||||
lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
describe('OrderDetailsPanel', () => {
|
||||
const panelBaseProps = {
|
||||
intl: fakeIntl,
|
||||
currentUser: createCurrentUser('user2'),
|
||||
totalMessages: 2,
|
||||
totalMessagePages: 1,
|
||||
oldestMessagePageFetched: 1,
|
||||
messages: [
|
||||
createMessage('msg1', {}, { sender: createUser('user1') }),
|
||||
createMessage('msg2', {}, { sender: createUser('user2') }),
|
||||
],
|
||||
initialMessageFailed: false,
|
||||
fetchMessagesInProgress: false,
|
||||
sendMessageInProgress: false,
|
||||
sendReviewInProgress: false,
|
||||
onManageDisableScrolling: noop,
|
||||
onOpenReviewModal: noop,
|
||||
onShowMoreMessages: noop,
|
||||
onSendMessage: noop,
|
||||
onSendReview: noop,
|
||||
onResetForm: noop,
|
||||
};
|
||||
|
||||
it('preauthorized matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<OrderDetailsPanelComponent transaction={txPreauthorized} {...panelBaseProps} />
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('accepted matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<OrderDetailsPanelComponent transaction={txAccepted} {...panelBaseProps} />
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('declined matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<OrderDetailsPanelComponent transaction={txDeclined} {...panelBaseProps} />
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('autodeclined matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<OrderDetailsPanelComponent transaction={txAutoDeclined} {...panelBaseProps} />
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('canceled matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<OrderDetailsPanelComponent transaction={txCanceled} {...panelBaseProps} />
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('delivered matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<OrderDetailsPanelComponent transaction={txDelivered} {...panelBaseProps} />
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders correct total price', () => {
|
||||
const { Money } = types;
|
||||
const tx = createTransaction({
|
||||
id: 'order-tx',
|
||||
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
total: new Money(16500, 'USD'),
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
provider: createUser('provider'),
|
||||
customer: createUser('customer'),
|
||||
});
|
||||
const panel = shallow(<OrderDetailsPanelComponent transaction={tx} {...panelBaseProps} />);
|
||||
const breakdownProps = panel.find(BookingBreakdown).props();
|
||||
expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD'));
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,459 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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 { isMobileSafari } from '../../util/userAgent';
|
||||
import {
|
||||
ActivityFeed,
|
||||
AvatarLarge,
|
||||
AvatarMedium,
|
||||
BookingBreakdown,
|
||||
NamedLink,
|
||||
PrimaryButton,
|
||||
ResponsiveImage,
|
||||
ReviewModal,
|
||||
SecondaryButton,
|
||||
} from '../../components';
|
||||
import { SendMessageForm } from '../../containers';
|
||||
|
||||
import css from './SaleDetailsPanel.css';
|
||||
|
||||
const breakdown = transaction => {
|
||||
const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
|
||||
|
||||
return loaded ? (
|
||||
<BookingBreakdown
|
||||
className={css.breakdown}
|
||||
userRole="provider"
|
||||
transaction={transaction}
|
||||
booking={transaction.booking}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const saleTitle = (transaction, listingLink, customerName) => {
|
||||
if (propTypes.txIsPreauthorized(transaction)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="SaleDetailsPanel.saleRequestedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else if (propTypes.txIsAccepted(transaction)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="SaleDetailsPanel.saleAcceptedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else if (propTypes.txIsDeclined(transaction)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="SaleDetailsPanel.saleDeclinedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else if (propTypes.txIsAutodeclined(transaction)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="SaleDetailsPanel.saleDeclinedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else if (propTypes.txIsCanceled(transaction)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="SaleDetailsPanel.saleCancelledTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
propTypes.txIsDelivered(transaction) ||
|
||||
propTypes.txHasFirstReview(transaction) ||
|
||||
propTypes.txIsReviewed(transaction)
|
||||
) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="SaleDetailsPanel.saleDeliveredTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const saleInfoText = (transaction, customerName) => {
|
||||
if (propTypes.txIsPreauthorized(transaction)) {
|
||||
return <FormattedMessage id="SaleDetailsPanel.saleRequestedInfo" values={{ customerName }} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export class SaleDetailsPanelComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sendMessageFormFocused: false,
|
||||
isReviewModalOpen: false,
|
||||
reviewSubmitted: false,
|
||||
};
|
||||
this.onOpenReviewModal = this.onOpenReviewModal.bind(this);
|
||||
this.onSubmitReview = this.onSubmitReview.bind(this);
|
||||
}
|
||||
|
||||
onOpenReviewModal() {
|
||||
this.setState({ isReviewModalOpen: true });
|
||||
}
|
||||
|
||||
onSubmitReview(values) {
|
||||
const { onSendReview, transaction } = this.props;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const { reviewRating, reviewContent } = values;
|
||||
const rating = Number.parseInt(reviewRating, 10);
|
||||
onSendReview(currentTransaction, rating, reviewContent)
|
||||
.then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true }))
|
||||
.catch(e => {
|
||||
// Do nothing.
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
currentUser,
|
||||
transaction,
|
||||
onAcceptSale,
|
||||
onDeclineSale,
|
||||
acceptInProgress,
|
||||
declineInProgress,
|
||||
acceptSaleError,
|
||||
declineSaleError,
|
||||
fetchMessagesInProgress,
|
||||
fetchMessagesError,
|
||||
totalMessagePages,
|
||||
oldestMessagePageFetched,
|
||||
messages,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
sendReviewInProgress,
|
||||
sendReviewError,
|
||||
onManageDisableScrolling,
|
||||
onShowMoreMessages,
|
||||
onSendMessage,
|
||||
onResetForm,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const currentCustomer = ensureUser(currentTransaction.customer);
|
||||
const currentProvider = ensureUser(currentTransaction.provider);
|
||||
const customerLoaded = !!currentCustomer.id;
|
||||
const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned;
|
||||
|
||||
const bannedUserDisplayName = intl.formatMessage({
|
||||
id: 'SaleDetailsPanel.bannedUserDisplayName',
|
||||
});
|
||||
|
||||
const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
|
||||
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
|
||||
|
||||
let otherUserDisplayName = '';
|
||||
const currentUserIsCustomer =
|
||||
currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
|
||||
const currentUserIsProvider =
|
||||
currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
|
||||
|
||||
if (currentUserIsCustomer) {
|
||||
otherUserDisplayName = authorDisplayName;
|
||||
} else if (currentUserIsProvider) {
|
||||
otherUserDisplayName = customerDisplayName;
|
||||
}
|
||||
|
||||
let listingLink = null;
|
||||
|
||||
if (currentListing.id && currentListing.attributes.title) {
|
||||
const title = currentListing.attributes.title;
|
||||
const params = { id: currentListing.id.uuid, slug: createSlug(title) };
|
||||
listingLink = (
|
||||
<NamedLink name="ListingPage" params={params}>
|
||||
{title}
|
||||
</NamedLink>
|
||||
);
|
||||
}
|
||||
|
||||
const bookingInfo = breakdown(currentTransaction);
|
||||
|
||||
const title = saleTitle(currentTransaction, listingLink, customerDisplayName);
|
||||
const infoText = isCustomerBanned
|
||||
? intl.formatMessage({
|
||||
id: 'SaleDetailsPanel.customerBannedStatus',
|
||||
})
|
||||
: saleInfoText(currentTransaction, customerDisplayName);
|
||||
const showInfoText = !!infoText;
|
||||
|
||||
const listingTitle = currentListing.attributes.title;
|
||||
const firstImage =
|
||||
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
|
||||
|
||||
const feedContainerClasses = classNames(css.feedContainer, {
|
||||
[css.feedContainerWithInfoAbove]: showInfoText,
|
||||
});
|
||||
const txTransitions = currentTransaction.attributes.transitions
|
||||
? currentTransaction.attributes.transitions
|
||||
: [];
|
||||
const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
|
||||
const handleShowOlderMessages = () => {
|
||||
onShowMoreMessages(currentTransaction.id);
|
||||
};
|
||||
const showFeed = messages.length > 0 || txTransitions.length > 0 || fetchMessagesError;
|
||||
const feedContainer = showFeed ? (
|
||||
<div className={feedContainerClasses}>
|
||||
<h3 className={css.feedHeading}>
|
||||
<FormattedMessage id="SaleDetailsPanel.activityHeading" />
|
||||
</h3>
|
||||
{fetchMessagesError ? (
|
||||
<p className={css.messagesError}>
|
||||
<FormattedMessage id="SaleDetailsPanel.messageLoadingFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
<ActivityFeed
|
||||
className={css.feed}
|
||||
messages={messages}
|
||||
transaction={currentTransaction}
|
||||
currentUser={currentUser}
|
||||
hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress}
|
||||
onOpenReviewModal={this.onOpenReviewModal}
|
||||
onShowOlderMessages={handleShowOlderMessages}
|
||||
fetchMessagesInProgress={fetchMessagesInProgress}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const canShowButtons = propTypes.txIsPreauthorized(currentTransaction) && !isCustomerBanned;
|
||||
const buttonsDisabled = acceptInProgress || declineInProgress;
|
||||
|
||||
const acceptErrorMessage = acceptSaleError ? (
|
||||
<p className={css.actionError}>
|
||||
<FormattedMessage id="SaleDetailsPanel.acceptSaleFailed" />
|
||||
</p>
|
||||
) : null;
|
||||
const declineErrorMessage = declineSaleError ? (
|
||||
<p className={css.actionError}>
|
||||
<FormattedMessage id="SaleDetailsPanel.declineSaleFailed" />
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
const actionButtonClasses = classNames(css.actionButtons, {
|
||||
[css.actionButtonsWithFormFocused]: this.state.sendMessageFormFocused,
|
||||
});
|
||||
const actionButtons = canShowButtons ? (
|
||||
<div className={actionButtonClasses}>
|
||||
<div className={css.actionErrors}>
|
||||
{acceptErrorMessage}
|
||||
{declineErrorMessage}
|
||||
</div>
|
||||
<div className={css.actionButtonWrapper}>
|
||||
<SecondaryButton
|
||||
inProgress={declineInProgress}
|
||||
disabled={buttonsDisabled}
|
||||
onClick={() => onDeclineSale(currentTransaction.id)}
|
||||
>
|
||||
<FormattedMessage id="SaleDetailsPanel.declineButton" />
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
inProgress={acceptInProgress}
|
||||
disabled={buttonsDisabled}
|
||||
onClick={() => onAcceptSale(currentTransaction.id)}
|
||||
>
|
||||
<FormattedMessage id="SaleDetailsPanel.acceptButton" />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const sendMessagePlaceholder = intl.formatMessage(
|
||||
{ id: 'SaleDetailsPanel.sendMessagePlaceholder' },
|
||||
{ name: otherUserDisplayName }
|
||||
);
|
||||
|
||||
const sendMessageFormName = 'SaleDetailsPanel.SendMessageForm';
|
||||
const scrollToMessage = messageId => {
|
||||
const selector = `#msg-${messageId.uuid}`;
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
el.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
const isMobSaf = isMobileSafari();
|
||||
const handleSendMessageFormFocus = () => {
|
||||
this.setState({ sendMessageFormFocused: true });
|
||||
if (isMobSaf) {
|
||||
// Scroll to bottom
|
||||
window.scroll({ top: document.body.scrollHeight, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
const handleSendMessageFormBlur = () => {
|
||||
this.setState({ sendMessageFormFocused: false });
|
||||
};
|
||||
const handleMessageSubmit = values => {
|
||||
const message = values.message ? values.message.trim() : null;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
onSendMessage(currentTransaction.id, message)
|
||||
.then(messageId => {
|
||||
onResetForm(sendMessageFormName);
|
||||
scrollToMessage(messageId);
|
||||
})
|
||||
.catch(e => {
|
||||
// Ignore, Redux handles the error
|
||||
});
|
||||
};
|
||||
const sendMessageFormClasses = classNames(css.sendMessageForm, {
|
||||
[css.sendMessageFormFixed]: this.state.sendMessageFormFocused || !canShowButtons,
|
||||
[css.sendMessageFormFocusedInMobileSafari]: isMobSaf && this.state.sendMessageFormFocused,
|
||||
});
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.container}>
|
||||
<div className={css.saleInfo}>
|
||||
<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={css.avatarWrapper}>
|
||||
<AvatarMedium user={currentCustomer} className={css.avatarMobile} />
|
||||
</div>
|
||||
<div className={css.avatarWrapper}>
|
||||
<AvatarLarge user={currentCustomer} className={css.avatarDesktop} />
|
||||
</div>
|
||||
<h1 className={css.title}>{title}</h1>
|
||||
{showInfoText ? <p className={css.infoText}>{infoText}</p> : null}
|
||||
<div className={css.breakdownContainerMobile}>
|
||||
<h3 className={css.breakdownTitleMobile}>
|
||||
<FormattedMessage id="SaleDetailsPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
{bookingInfo}
|
||||
</div>
|
||||
{feedContainer}
|
||||
<SendMessageForm
|
||||
form={sendMessageFormName}
|
||||
rootClassName={sendMessageFormClasses}
|
||||
messagePlaceholder={sendMessagePlaceholder}
|
||||
inProgress={sendMessageInProgress}
|
||||
sendMessageError={sendMessageError}
|
||||
onFocus={handleSendMessageFormFocus}
|
||||
onBlur={handleSendMessageFormBlur}
|
||||
onSubmit={handleMessageSubmit}
|
||||
/>
|
||||
<div className={css.mobileActionButtons}>{actionButtons}</div>
|
||||
</div>
|
||||
<div className={css.desktopAside}>
|
||||
<div className={css.breakdownContainerDesktop}>
|
||||
<div className={css.breakdownImageWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
rootClassName={css.rootForImage}
|
||||
alt={listingTitle}
|
||||
image={firstImage}
|
||||
nameSet={[
|
||||
{ name: 'landscape-crop', size: '400w' },
|
||||
{ name: 'landscape-crop2x', size: '800w' },
|
||||
]}
|
||||
sizes="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className={css.breakdownTitleDesktop}>
|
||||
<FormattedMessage id="SaleDetailsPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
<div className={css.breakdownDesktop}>{bookingInfo}</div>
|
||||
<div className={css.desktopActionButtons}>{actionButtons}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReviewModal
|
||||
id="ReviewSaleModal"
|
||||
isOpen={this.state.isReviewModalOpen}
|
||||
onCloseModal={() => this.setState({ isReviewModalOpen: false })}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
onSubmitReview={this.onSubmitReview}
|
||||
revieweeName={customerDisplayName}
|
||||
reviewSent={this.state.reviewSubmitted}
|
||||
sendReviewInProgress={sendReviewInProgress}
|
||||
sendReviewError={sendReviewError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SaleDetailsPanelComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
currentUser: null,
|
||||
acceptSaleError: null,
|
||||
declineSaleError: null,
|
||||
fetchMessagesError: null,
|
||||
sendMessageError: null,
|
||||
sendReviewError: null,
|
||||
};
|
||||
|
||||
const { bool, func, string, arrayOf, number } = PropTypes;
|
||||
|
||||
SaleDetailsPanelComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
|
||||
currentUser: propTypes.currentUser,
|
||||
transaction: propTypes.transaction.isRequired,
|
||||
onAcceptSale: func.isRequired,
|
||||
onDeclineSale: func.isRequired,
|
||||
acceptInProgress: bool.isRequired,
|
||||
declineInProgress: bool.isRequired,
|
||||
acceptSaleError: propTypes.error,
|
||||
declineSaleError: propTypes.error,
|
||||
fetchMessagesInProgress: bool.isRequired,
|
||||
fetchMessagesError: propTypes.error,
|
||||
totalMessagePages: number.isRequired,
|
||||
oldestMessagePageFetched: number.isRequired,
|
||||
messages: arrayOf(propTypes.message).isRequired,
|
||||
sendMessageInProgress: bool.isRequired,
|
||||
sendMessageError: propTypes.error,
|
||||
sendReviewInProgress: bool.isRequired,
|
||||
sendReviewError: propTypes.error,
|
||||
onManageDisableScrolling: func.isRequired,
|
||||
onShowMoreMessages: func.isRequired,
|
||||
onSendMessage: func.isRequired,
|
||||
onSendReview: func.isRequired,
|
||||
onResetForm: func.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const SaleDetailsPanel = injectIntl(SaleDetailsPanelComponent);
|
||||
|
||||
export default SaleDetailsPanel;
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import {
|
||||
createTransaction,
|
||||
createBooking,
|
||||
createListing,
|
||||
createUser,
|
||||
createCurrentUser,
|
||||
createMessage,
|
||||
} from '../../util/test-data';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { BookingBreakdown } from '../../components';
|
||||
import { SaleDetailsPanelComponent } from './SaleDetailsPanel.js';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
const { Money } = types;
|
||||
|
||||
const baseTxAttrs = {
|
||||
total: new Money(16500, 'USD'),
|
||||
commission: new Money(1000, 'USD'),
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
customer: createUser('customer1'),
|
||||
lastTransitionedAt: new Date(Date.UTC(2017, 5, 10)),
|
||||
};
|
||||
const txPreauthorized = createTransaction({
|
||||
id: 'sale-preauthorized',
|
||||
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txAccepted = createTransaction({
|
||||
id: 'sale-accepted',
|
||||
lastTransition: propTypes.TX_TRANSITION_ACCEPT,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txDeclined = createTransaction({
|
||||
id: 'sale-declined',
|
||||
lastTransition: propTypes.TX_TRANSITION_DECLINE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txAutoDeclined = createTransaction({
|
||||
id: 'sale-autodeclined',
|
||||
lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txCanceled = createTransaction({
|
||||
id: 'sale-canceled',
|
||||
lastTransition: propTypes.TX_TRANSITION_CANCELED,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txDelivered = createTransaction({
|
||||
id: 'sale-delivered',
|
||||
lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
describe('SaleDetailsPanel', () => {
|
||||
const panelBaseProps = {
|
||||
onAcceptSale: noop,
|
||||
onDeclineSale: noop,
|
||||
acceptInProgress: false,
|
||||
declineInProgress: false,
|
||||
currentUser: createCurrentUser('user1'),
|
||||
totalMessages: 2,
|
||||
totalMessagePages: 1,
|
||||
oldestMessagePageFetched: 1,
|
||||
messages: [
|
||||
createMessage('msg1', {}, { sender: createUser('user1') }),
|
||||
createMessage('msg2', {}, { sender: createUser('user2') }),
|
||||
],
|
||||
fetchMessagesInProgress: false,
|
||||
sendMessageInProgress: false,
|
||||
sendReviewInProgress: false,
|
||||
onManageDisableScrolling: noop,
|
||||
onOpenReviewModal: noop,
|
||||
onShowMoreMessages: noop,
|
||||
onSendMessage: noop,
|
||||
onSendReview: noop,
|
||||
onResetForm: noop,
|
||||
intl: fakeIntl,
|
||||
};
|
||||
|
||||
it('preauthorized matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txPreauthorized,
|
||||
};
|
||||
const tree = renderShallow(<SaleDetailsPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('accepted matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txAccepted,
|
||||
};
|
||||
const tree = renderShallow(<SaleDetailsPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('declined matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txDeclined,
|
||||
};
|
||||
const tree = renderShallow(<SaleDetailsPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('autodeclined matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txAutoDeclined,
|
||||
};
|
||||
const tree = renderShallow(<SaleDetailsPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('canceled matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txCanceled,
|
||||
};
|
||||
const tree = renderShallow(<SaleDetailsPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('delivered matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txDelivered,
|
||||
};
|
||||
const tree = renderShallow(<SaleDetailsPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders correct total price', () => {
|
||||
const transaction = createTransaction({
|
||||
id: 'sale-tx',
|
||||
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
total: new Money(16500, 'USD'),
|
||||
commission: new Money(1000, 'USD'),
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
customer: createUser('customer1'),
|
||||
lastTransitionedAt: new Date(Date.UTC(2017, 5, 10)),
|
||||
});
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction,
|
||||
};
|
||||
const panel = shallow(<SaleDetailsPanelComponent {...props} />);
|
||||
const breakdownProps = panel.find(BookingBreakdown).props();
|
||||
|
||||
// Total price for the provider should be transaction total minus the commission.
|
||||
expect(breakdownProps.transaction.attributes.payoutTotal).toEqual(new Money(15500, 'USD'));
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -17,19 +17,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.saleInfo {
|
||||
margin-bottom: 104px;
|
||||
.txInfo {
|
||||
margin-bottom: 210px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
max-width: 542px;
|
||||
flex-basis: 538px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
margin-right: 56px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.imageWrapperMobile {
|
||||
/* Layout */
|
||||
display: block;
|
||||
|
|
@ -41,25 +40,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Firefox doesn't support image aspect ratio inside flexbox */
|
||||
.aspectWrapper {
|
||||
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
|
||||
background-color: var(--matterColorNegative); /* Loading BG color */
|
||||
}
|
||||
|
||||
.aspectWrapperMobile {
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.aspectWrapperDesktop {
|
||||
display: none;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.rootForImage {
|
||||
/* Layout - image will take space defined by aspect ratio wrapper */
|
||||
position: absolute;
|
||||
|
|
@ -68,68 +54,33 @@
|
|||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 22px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
max-width: 100%;
|
||||
margin: 42px 0 0 0;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.infoText {
|
||||
margin: 18px 24px 0 24px;
|
||||
.avatarWrapperMobile {
|
||||
/* Position (over the listing image)*/
|
||||
margin-left: 24px;
|
||||
margin-top: -31px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 23px 24px 0 24px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 22px 0 -2px 0;
|
||||
}
|
||||
/* Rendering context to the same lavel as listing image */
|
||||
position: relative;
|
||||
|
||||
/* Layout */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.messagesError {
|
||||
color: var(--failColor);
|
||||
margin: 13px 0 0 0;
|
||||
}
|
||||
|
||||
.actionError {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
color: var(--failColor);
|
||||
margin: 0 0 11px 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.actionErrors {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
.avatarWrapperCustomerDesktop {
|
||||
composes: avatarWrapperMobile;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
position: absolute;
|
||||
top: 151px;
|
||||
margin-left: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarMobile {
|
||||
/* Position (over the listing image)*/
|
||||
margin-top: -31px;
|
||||
margin-left: 24px;
|
||||
|
||||
/* Bring on top of image */
|
||||
position: relative;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -144,22 +95,57 @@
|
|||
}
|
||||
}
|
||||
|
||||
.breakdown {
|
||||
margin: 14px 0 0 0;
|
||||
.headingOrder {
|
||||
margin: 29px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 18px 0 0 0;
|
||||
margin: 25px 24px 0 24px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin: 14px 0 0 0;
|
||||
max-width: 100%;
|
||||
margin: 177px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownContainerMobile {
|
||||
margin: 47px 24px 0 24px;
|
||||
.headingSale {
|
||||
margin: 22px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 44px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
max-width: 100%;
|
||||
margin: 42px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mainTitle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.transactionInfoMessage {
|
||||
margin: 18px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 23px 24px 0 24px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 23px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.transitionDate {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.breakdownMobile {
|
||||
margin-top: 47px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 43px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
|
|
@ -167,21 +153,28 @@
|
|||
}
|
||||
|
||||
.desktopAside {
|
||||
display: none;
|
||||
margin: 1px 0 0 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
margin: 120px 0 0 0;
|
||||
margin-top: 123px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownContainerDesktop {
|
||||
.breakdownDesktop {
|
||||
display: none;
|
||||
|
||||
position: sticky;
|
||||
top: -272px; /* This is a hack to showcase how the component would look when the image isn't sticky */
|
||||
width: 409px;
|
||||
background-color: var(--matterColorLight);
|
||||
border: 1px solid var(--matterColorNegative);
|
||||
border-radius: 2px;
|
||||
width: 409px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownImageWrapper {
|
||||
|
|
@ -191,27 +184,62 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.breakdownTitleMobile {
|
||||
/* Font */
|
||||
color: var(--matterColorAnti);
|
||||
margin: 0;
|
||||
.breakdownHeadings {
|
||||
display: none;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 0;
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
margin-left: 48px;
|
||||
margin-right: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownTitleDesktop {
|
||||
margin: 44px 48px 18px 48px;
|
||||
color: var(--matterColorAnti);
|
||||
.breakdownTitle {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdownDesktop {
|
||||
margin: 0 48px 47px 48px;
|
||||
.breakdownSubtitle {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 1px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bookingBreakdownTitle {
|
||||
/* Font */
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
margin: 0 24px 0 24px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin: 37px 48px 26px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.breakdown {
|
||||
margin: 14px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 18px 24px 0 24px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 14px 48px 47px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.feedContainer {
|
||||
margin: 46px 24px 36px 24px;
|
||||
margin: 46px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 46px 24px 0 24px;
|
||||
|
|
@ -292,7 +320,7 @@
|
|||
position: static;
|
||||
box-shadow: none;
|
||||
width: auto;
|
||||
margin: 0 48px 48px 48px;
|
||||
margin: 0 48px 0 48px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -310,6 +338,7 @@
|
|||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -341,3 +370,39 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageError {
|
||||
color: var(--failColor);
|
||||
margin: 13px 0 22px 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 13px 0 23px 0;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin: 12px 0 23px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.actionError {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
color: var(--failColor);
|
||||
margin: 0 0 11px 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.actionErrors {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
position: absolute;
|
||||
top: 151px;
|
||||
}
|
||||
}
|
||||
426
src/components/TransactionPanel/TransactionPanel.helpers.js
Normal file
426
src/components/TransactionPanel/TransactionPanel.helpers.js
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { userDisplayName } from '../../util/data';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
import {
|
||||
ActivityFeed,
|
||||
BookingBreakdown,
|
||||
NamedLink,
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
} from '../../components';
|
||||
import css from './TransactionPanel.css';
|
||||
|
||||
// Functional component as a helper to build ActivityFeed section
|
||||
export const FeedSection = props => {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
currentTransaction,
|
||||
currentUser,
|
||||
fetchMessagesError,
|
||||
fetchMessagesInProgress,
|
||||
initialMessageFailed,
|
||||
messages,
|
||||
oldestMessagePageFetched,
|
||||
onShowMoreMessages,
|
||||
onOpenReviewModal,
|
||||
totalMessagePages,
|
||||
} = props;
|
||||
|
||||
const txTransitions = currentTransaction.attributes.transitions
|
||||
? currentTransaction.attributes.transitions
|
||||
: [];
|
||||
const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
|
||||
|
||||
const showFeed =
|
||||
messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError;
|
||||
|
||||
const classes = classNames(rootClassName || css.feedContainer, className);
|
||||
|
||||
return showFeed ? (
|
||||
<div className={classes}>
|
||||
<h3 className={css.feedHeading}>
|
||||
<FormattedMessage id="TransactionPanel.activityHeading" />
|
||||
</h3>
|
||||
{initialMessageFailed ? (
|
||||
<p className={css.messageError}>
|
||||
<FormattedMessage id="TransactionPanel.initialMessageFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
{fetchMessagesError ? (
|
||||
<p className={css.messageError}>
|
||||
<FormattedMessage id="TransactionPanel.messageLoadingFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
<ActivityFeed
|
||||
className={css.feed}
|
||||
messages={messages}
|
||||
transaction={currentTransaction}
|
||||
currentUser={currentUser}
|
||||
hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress}
|
||||
onOpenReviewModal={onOpenReviewModal}
|
||||
onShowOlderMessages={() => {
|
||||
onShowMoreMessages(currentTransaction.id);
|
||||
}}
|
||||
fetchMessagesInProgress={fetchMessagesInProgress}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
// Functional component as a helper to build BookingBreakdown
|
||||
export const BreakdownMaybe = props => {
|
||||
const { className, rootClassName, transaction, transactionRole } = props;
|
||||
const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
|
||||
|
||||
const classes = classNames(rootClassName || css.breakdown, className);
|
||||
return loaded ? (
|
||||
<div>
|
||||
<h3 className={css.bookingBreakdownTitle}>
|
||||
<FormattedMessage id="TransactionPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
<BookingBreakdown
|
||||
className={classes}
|
||||
userRole={transactionRole}
|
||||
transaction={transaction}
|
||||
booking={transaction.booking}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
// Functional component as a helper to build ActionButtons
|
||||
// Provider only when state is preauthorized
|
||||
export const ActionButtonsMaybe = props => {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
canShowButtons,
|
||||
transaction,
|
||||
acceptInProgress,
|
||||
declineInProgress,
|
||||
acceptSaleError,
|
||||
declineSaleError,
|
||||
onAcceptSale,
|
||||
onDeclineSale,
|
||||
} = props;
|
||||
|
||||
const buttonsDisabled = acceptInProgress || declineInProgress;
|
||||
|
||||
const acceptErrorMessage = acceptSaleError ? (
|
||||
<p className={css.actionError}>
|
||||
<FormattedMessage id="TransactionPanel.acceptSaleFailed" />
|
||||
</p>
|
||||
) : null;
|
||||
const declineErrorMessage = declineSaleError ? (
|
||||
<p className={css.actionError}>
|
||||
<FormattedMessage id="TransactionPanel.declineSaleFailed" />
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
const classes = classNames(rootClassName || css.actionButtons, className);
|
||||
|
||||
return canShowButtons ? (
|
||||
<div className={classes}>
|
||||
<div className={css.actionErrors}>
|
||||
{acceptErrorMessage}
|
||||
{declineErrorMessage}
|
||||
</div>
|
||||
<div className={css.actionButtonWrapper}>
|
||||
<SecondaryButton
|
||||
inProgress={declineInProgress}
|
||||
disabled={buttonsDisabled}
|
||||
onClick={() => onDeclineSale(transaction.id)}
|
||||
>
|
||||
<FormattedMessage id="TransactionPanel.declineButton" />
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
inProgress={acceptInProgress}
|
||||
disabled={buttonsDisabled}
|
||||
onClick={() => onAcceptSale(transaction.id)}
|
||||
>
|
||||
<FormattedMessage id="TransactionPanel.acceptButton" />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const createListingLink = (listingLoaded, title, id) => {
|
||||
if (listingLoaded && title) {
|
||||
const params = { id: id.uuid, slug: createSlug(title) };
|
||||
return (
|
||||
<NamedLink name="ListingPage" params={params}>
|
||||
{title}
|
||||
</NamedLink>
|
||||
);
|
||||
} else {
|
||||
return <FormattedMessage id="TransactionPanel.deletedListingOrderTitle" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Functional component as a helper to build order title
|
||||
export const OrderTitle = props => {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
transaction,
|
||||
customerDisplayName: customerName,
|
||||
currentListing,
|
||||
listingTitle,
|
||||
} = props;
|
||||
const listingLoaded = !!currentListing.id;
|
||||
const listingLink = createListingLink(listingLoaded, listingTitle, currentListing.id);
|
||||
|
||||
const classes = classNames(rootClassName || css.headingOrder, className);
|
||||
|
||||
if (propTypes.txIsPreauthorized(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<span className={css.mainTitle}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.orderPreauthorizedTitle"
|
||||
values={{ customerName }}
|
||||
/>
|
||||
</span>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.orderPreauthorizedSubtitle"
|
||||
values={{ listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (propTypes.txIsAccepted(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<span className={css.mainTitle}>
|
||||
<FormattedMessage id="TransactionPanel.orderAcceptedTitle" values={{ customerName }} />
|
||||
</span>
|
||||
<FormattedMessage id="TransactionPanel.orderAcceptedSubtitle" values={{ listingLink }} />
|
||||
</h1>
|
||||
);
|
||||
} else if (propTypes.txIsDeclined(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.orderDeclinedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (propTypes.txIsAutodeclined(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.orderDeclinedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (propTypes.txIsCanceled(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.orderCancelledTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (
|
||||
propTypes.txIsDelivered(transaction) ||
|
||||
propTypes.txHasFirstReview(transaction) ||
|
||||
propTypes.txIsReviewed(transaction)
|
||||
) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.orderDeliveredTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Functional component as a helper to build order message below title
|
||||
export const OrderMessage = props => {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
transaction,
|
||||
authorDisplayName: providerName,
|
||||
listingDeleted,
|
||||
} = props;
|
||||
const classes = classNames(rootClassName || css.transactionInfoMessage, className);
|
||||
|
||||
if (!listingDeleted && propTypes.txIsPreauthorized(transaction)) {
|
||||
return (
|
||||
<p className={classes}>
|
||||
<FormattedMessage id="TransactionPanel.orderPreauthorizedInfo" values={{ providerName }} />
|
||||
</p>
|
||||
);
|
||||
} else if (listingDeleted) {
|
||||
return (
|
||||
<p className={classes}>
|
||||
<FormattedMessage id="TransactionPanel.messageDeletedListing" />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Functional component as a helper to build sale title
|
||||
export const SaleTitle = props => {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
transaction,
|
||||
customerDisplayName: customerName,
|
||||
currentListing,
|
||||
listingTitle,
|
||||
} = props;
|
||||
const listingLoaded = !!currentListing.id;
|
||||
const listingLink = createListingLink(listingLoaded, listingTitle, currentListing.id);
|
||||
|
||||
const classes = classNames(rootClassName || css.headingSale, className);
|
||||
|
||||
if (propTypes.txIsPreauthorized(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.saleRequestedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (propTypes.txIsAccepted(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.saleAcceptedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (propTypes.txIsDeclined(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.saleDeclinedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (propTypes.txIsAutodeclined(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.saleDeclinedTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (propTypes.txIsCanceled(transaction)) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.saleCancelledTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else if (
|
||||
propTypes.txIsDelivered(transaction) ||
|
||||
propTypes.txHasFirstReview(transaction) ||
|
||||
propTypes.txIsReviewed(transaction)
|
||||
) {
|
||||
return (
|
||||
<h1 className={classes}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.saleDeliveredTitle"
|
||||
values={{ customerName, listingLink }}
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Functional component as a helper to build sale message below title
|
||||
export const SaleMessage = props => {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
transaction,
|
||||
customerDisplayName: customerName,
|
||||
isCustomerBanned,
|
||||
} = props;
|
||||
const classes = classNames(rootClassName || css.transactionInfoMessage, className);
|
||||
|
||||
if (!isCustomerBanned && propTypes.txIsPreauthorized(transaction)) {
|
||||
return (
|
||||
<p className={classes}>
|
||||
<FormattedMessage id="TransactionPanel.saleRequestedInfo" values={{ customerName }} />
|
||||
</p>
|
||||
);
|
||||
} else if (isCustomerBanned) {
|
||||
return (
|
||||
<p className={classes}>
|
||||
<FormattedMessage id="TransactionPanel.customerBannedStatus" />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Functional component as a helper to choose and show Order or Sale title
|
||||
export const TransactionPageTitle = props => {
|
||||
return props.transactionRole === 'customer' ? (
|
||||
<OrderTitle {...props} />
|
||||
) : (
|
||||
<SaleTitle {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
// Functional component as a helper to choose and show Order or Sale message
|
||||
export const TransactionPageMessage = props => {
|
||||
return props.transactionRole === 'customer' ? (
|
||||
<OrderMessage {...props} />
|
||||
) : (
|
||||
<SaleMessage {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to get display names for different roles
|
||||
export const displayNames = (
|
||||
currentUser,
|
||||
currentProvider,
|
||||
currentCustomer,
|
||||
bannedUserDisplayName
|
||||
) => {
|
||||
const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
|
||||
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
|
||||
|
||||
let otherUserDisplayName = '';
|
||||
const currentUserIsCustomer =
|
||||
currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
|
||||
const currentUserIsProvider =
|
||||
currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
|
||||
|
||||
if (currentUserIsCustomer) {
|
||||
otherUserDisplayName = authorDisplayName;
|
||||
} else if (currentUserIsProvider) {
|
||||
otherUserDisplayName = customerDisplayName;
|
||||
}
|
||||
|
||||
return {
|
||||
authorDisplayName,
|
||||
customerDisplayName,
|
||||
otherUserDisplayName,
|
||||
};
|
||||
};
|
||||
387
src/components/TransactionPanel/TransactionPanel.js
Normal file
387
src/components/TransactionPanel/TransactionPanel.js
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { ensureListing, ensureTransaction, ensureUser } from '../../util/data';
|
||||
import { isMobileSafari } from '../../util/userAgent';
|
||||
import { AvatarMedium, AvatarLarge, ResponsiveImage, ReviewModal } from '../../components';
|
||||
import { SendMessageForm } from '../../containers';
|
||||
|
||||
// These are internal components that make this file more readable.
|
||||
import {
|
||||
ActionButtonsMaybe,
|
||||
BreakdownMaybe,
|
||||
FeedSection,
|
||||
TransactionPageTitle,
|
||||
TransactionPageMessage,
|
||||
displayNames,
|
||||
} from './TransactionPanel.helpers';
|
||||
import css from './TransactionPanel.css';
|
||||
|
||||
export class TransactionPanelComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sendMessageFormFocused: false,
|
||||
isReviewModalOpen: false,
|
||||
reviewSubmitted: false,
|
||||
};
|
||||
this.isMobSaf = false;
|
||||
this.sendMessageFormName = 'TransactionPanel.SendMessageForm';
|
||||
|
||||
this.onOpenReviewModal = this.onOpenReviewModal.bind(this);
|
||||
this.onSubmitReview = this.onSubmitReview.bind(this);
|
||||
this.onSendMessageFormFocus = this.onSendMessageFormFocus.bind(this);
|
||||
this.onSendMessageFormBlur = this.onSendMessageFormBlur.bind(this);
|
||||
this.onMessageSubmit = this.onMessageSubmit.bind(this);
|
||||
this.scrollToMessage = this.scrollToMessage.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.isMobSaf = isMobileSafari();
|
||||
}
|
||||
|
||||
onOpenReviewModal() {
|
||||
this.setState({ isReviewModalOpen: true });
|
||||
}
|
||||
|
||||
onSubmitReview(values) {
|
||||
const { onSendReview, transaction } = this.props;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const { reviewRating, reviewContent } = values;
|
||||
const rating = Number.parseInt(reviewRating, 10);
|
||||
onSendReview(currentTransaction, rating, reviewContent)
|
||||
.then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true }))
|
||||
.catch(e => {
|
||||
// Do nothing.
|
||||
});
|
||||
}
|
||||
|
||||
onSendMessageFormFocus() {
|
||||
this.setState({ sendMessageFormFocused: true });
|
||||
if (this.isMobSaf) {
|
||||
// Scroll to bottom
|
||||
window.scroll({ top: document.body.scrollHeight, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
onSendMessageFormBlur() {
|
||||
this.setState({ sendMessageFormFocused: false });
|
||||
}
|
||||
|
||||
onMessageSubmit(values) {
|
||||
const message = values.message ? values.message.trim() : null;
|
||||
const { transaction, onResetForm, onSendMessage } = this.props;
|
||||
const ensuredTransaction = ensureTransaction(transaction);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
onSendMessage(ensuredTransaction.id, message)
|
||||
.then(messageId => {
|
||||
onResetForm(this.sendMessageFormName);
|
||||
this.scrollToMessage(messageId);
|
||||
})
|
||||
.catch(e => {
|
||||
// Ignore, Redux handles the error
|
||||
});
|
||||
}
|
||||
|
||||
scrollToMessage(messageId) {
|
||||
const selector = `#msg-${messageId.uuid}`;
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
el.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
currentUser,
|
||||
transaction,
|
||||
totalMessagePages,
|
||||
oldestMessagePageFetched,
|
||||
messages,
|
||||
initialMessageFailed,
|
||||
fetchMessagesInProgress,
|
||||
fetchMessagesError,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
sendReviewInProgress,
|
||||
sendReviewError,
|
||||
onManageDisableScrolling,
|
||||
onShowMoreMessages,
|
||||
transactionRole,
|
||||
intl,
|
||||
onAcceptSale,
|
||||
onDeclineSale,
|
||||
acceptInProgress,
|
||||
declineInProgress,
|
||||
acceptSaleError,
|
||||
declineSaleError,
|
||||
} = this.props;
|
||||
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const currentProvider = ensureUser(currentTransaction.provider);
|
||||
const currentCustomer = ensureUser(currentTransaction.customer);
|
||||
const isCustomer = transactionRole === 'customer';
|
||||
const isProvider = transactionRole === 'provider';
|
||||
|
||||
const listingLoaded = !!currentListing.id;
|
||||
const listingDeleted = listingLoaded && currentListing.attributes.deleted;
|
||||
const customerLoaded = !!currentCustomer.id;
|
||||
const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned;
|
||||
const canShowButtons =
|
||||
isProvider && propTypes.txIsPreauthorized(currentTransaction) && !isCustomerBanned;
|
||||
|
||||
const bannedUserDisplayName = intl.formatMessage({
|
||||
id: 'TransactionPanel.bannedUserDisplayName',
|
||||
});
|
||||
const deletedListingTitle = intl.formatMessage({
|
||||
id: 'TransactionPanel.deletedListingTitle',
|
||||
});
|
||||
|
||||
const { authorDisplayName, customerDisplayName, otherUserDisplayName } = displayNames(
|
||||
currentUser,
|
||||
currentProvider,
|
||||
currentCustomer,
|
||||
bannedUserDisplayName
|
||||
);
|
||||
|
||||
const listingTitle = currentListing.attributes.deleted
|
||||
? deletedListingTitle
|
||||
: currentListing.attributes.title;
|
||||
|
||||
const firstImage =
|
||||
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
|
||||
|
||||
const actionButtonClasses = classNames(css.actionButtons, {
|
||||
[css.actionButtonsWithFormFocused]: this.state.sendMessageFormFocused,
|
||||
});
|
||||
|
||||
const actionButtons = (
|
||||
<ActionButtonsMaybe
|
||||
rootClassName={actionButtonClasses}
|
||||
canShowButtons={canShowButtons}
|
||||
transaction={currentTransaction}
|
||||
acceptInProgress={acceptInProgress}
|
||||
declineInProgress={declineInProgress}
|
||||
acceptSaleError={acceptSaleError}
|
||||
declineSaleError={declineSaleError}
|
||||
onAcceptSale={onAcceptSale}
|
||||
onDeclineSale={onDeclineSale}
|
||||
/>
|
||||
);
|
||||
|
||||
const sendMessagePlaceholder = intl.formatMessage(
|
||||
{ id: 'TransactionPanel.sendMessagePlaceholder' },
|
||||
{ name: otherUserDisplayName }
|
||||
);
|
||||
|
||||
const sendMessageFormClasses = classNames(css.sendMessageForm, {
|
||||
[css.sendMessageFormFixed]: !canShowButtons || this.state.sendMessageFormFocused,
|
||||
[css.sendMessageFormFocusedInMobileSafari]:
|
||||
this.isMobSaf && this.state.sendMessageFormFocused,
|
||||
});
|
||||
|
||||
const showInfoMessage =
|
||||
listingDeleted || (!listingDeleted && propTypes.txIsPreauthorized(transaction)); // !!orderInfoMessage;
|
||||
|
||||
const feedContainerClasses = classNames(css.feedContainer, {
|
||||
[css.feedContainerWithInfoAbove]: showInfoMessage,
|
||||
});
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.container}>
|
||||
<div className={css.txInfo}>
|
||||
<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.avatarWrapperMobile, css.avatarMobile)}>
|
||||
<AvatarMedium user={currentProvider} />
|
||||
</div>
|
||||
{isProvider ? (
|
||||
<div className={css.avatarWrapper}>
|
||||
<AvatarLarge user={currentCustomer} className={css.avatarDesktop} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<TransactionPageTitle
|
||||
transaction={currentTransaction}
|
||||
customerDisplayName={customerDisplayName}
|
||||
currentListing={currentListing}
|
||||
listingTitle={listingTitle}
|
||||
transactionRole={transactionRole}
|
||||
/>
|
||||
<TransactionPageMessage
|
||||
transaction={currentTransaction}
|
||||
authorDisplayName={authorDisplayName}
|
||||
customerDisplayName={customerDisplayName}
|
||||
listingDeleted={listingDeleted}
|
||||
isCustomerBanned={isCustomerBanned}
|
||||
transactionRole={transactionRole}
|
||||
/>
|
||||
|
||||
<div className={css.breakdownMobile}>
|
||||
<h3 className={css.bookingBreakdownTitle}>
|
||||
<FormattedMessage id="TransactionPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
<BreakdownMaybe transaction={currentTransaction} transactionRole={transactionRole} />
|
||||
</div>
|
||||
|
||||
<FeedSection
|
||||
rootClassName={feedContainerClasses}
|
||||
currentTransaction={currentTransaction}
|
||||
currentUser={currentUser}
|
||||
fetchMessagesError={fetchMessagesError}
|
||||
fetchMessagesInProgress={fetchMessagesInProgress}
|
||||
initialMessageFailed={initialMessageFailed}
|
||||
messages={messages}
|
||||
oldestMessagePageFetched={oldestMessagePageFetched}
|
||||
onOpenReviewModal={this.onOpenReviewModal}
|
||||
onShowMoreMessages={onShowMoreMessages}
|
||||
totalMessagePages={totalMessagePages}
|
||||
/>
|
||||
|
||||
<SendMessageForm
|
||||
form={this.sendMessageFormName}
|
||||
rootClassName={sendMessageFormClasses}
|
||||
messagePlaceholder={sendMessagePlaceholder}
|
||||
inProgress={sendMessageInProgress}
|
||||
sendMessageError={sendMessageError}
|
||||
onFocus={this.onSendMessageFormFocus}
|
||||
onBlur={this.onSendMessageFormBlur}
|
||||
onSubmit={this.onMessageSubmit}
|
||||
/>
|
||||
{isProvider && canShowButtons ? (
|
||||
<div className={css.mobileActionButtons}>{actionButtons}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={css.desktopAside}>
|
||||
<div className={css.breakdownDesktop}>
|
||||
<div className={css.breakdownImageWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
rootClassName={css.rootForImage}
|
||||
alt={listingTitle}
|
||||
image={firstImage}
|
||||
nameSet={[
|
||||
{ name: 'landscape-crop', size: '400w' },
|
||||
{ name: 'landscape-crop2x', size: '800w' },
|
||||
]}
|
||||
sizes="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isCustomer ? (
|
||||
<div className={css.avatarWrapperCustomerDesktop}>
|
||||
<AvatarMedium user={currentProvider} />
|
||||
</div>
|
||||
) : null}
|
||||
{isCustomer ? (
|
||||
<div className={css.breakdownHeadings}>
|
||||
<h2 className={css.breakdownTitle}>{listingTitle}</h2>
|
||||
<p className={css.breakdownSubtitle}>
|
||||
<FormattedMessage
|
||||
id="TransactionPanel.hostedBy"
|
||||
values={{ name: authorDisplayName }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<BreakdownMaybe transaction={currentTransaction} transactionRole={transactionRole} />
|
||||
{isProvider && canShowButtons ? (
|
||||
<div className={css.desktopActionButtons}>{actionButtons}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReviewModal
|
||||
id="ReviewOrderModal"
|
||||
isOpen={this.state.isReviewModalOpen}
|
||||
onCloseModal={() => this.setState({ isReviewModalOpen: false })}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
onSubmitReview={this.onSubmitReview}
|
||||
revieweeName={authorDisplayName}
|
||||
reviewSent={this.state.reviewSubmitted}
|
||||
sendReviewInProgress={sendReviewInProgress}
|
||||
sendReviewError={sendReviewError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TransactionPanelComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
currentUser: null,
|
||||
acceptSaleError: null,
|
||||
declineSaleError: null,
|
||||
fetchMessagesError: null,
|
||||
initialMessageFailed: null,
|
||||
sendMessageError: null,
|
||||
sendReviewError: null,
|
||||
};
|
||||
|
||||
const { arrayOf, bool, func, number, string } = PropTypes;
|
||||
|
||||
TransactionPanelComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
|
||||
currentUser: propTypes.currentUser,
|
||||
transaction: propTypes.transaction.isRequired,
|
||||
totalMessagePages: number.isRequired,
|
||||
oldestMessagePageFetched: number.isRequired,
|
||||
messages: arrayOf(propTypes.message).isRequired,
|
||||
initialMessageFailed: bool,
|
||||
fetchMessagesInProgress: bool.isRequired,
|
||||
fetchMessagesError: propTypes.error,
|
||||
sendMessageInProgress: bool.isRequired,
|
||||
sendMessageError: propTypes.error,
|
||||
sendReviewInProgress: bool.isRequired,
|
||||
sendReviewError: propTypes.error,
|
||||
onManageDisableScrolling: func.isRequired,
|
||||
onShowMoreMessages: func.isRequired,
|
||||
onSendMessage: func.isRequired,
|
||||
onSendReview: func.isRequired,
|
||||
onResetForm: func.isRequired,
|
||||
|
||||
// Sale related props
|
||||
onAcceptSale: func.isRequired,
|
||||
onDeclineSale: func.isRequired,
|
||||
acceptInProgress: bool.isRequired,
|
||||
declineInProgress: bool.isRequired,
|
||||
acceptSaleError: propTypes.error,
|
||||
declineSaleError: propTypes.error,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
const TransactionPanel = injectIntl(TransactionPanelComponent);
|
||||
|
||||
export default TransactionPanel;
|
||||
321
src/components/TransactionPanel/TransactionPanel.test.js
Normal file
321
src/components/TransactionPanel/TransactionPanel.test.js
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import {
|
||||
createTransaction,
|
||||
createBooking,
|
||||
createListing,
|
||||
createUser,
|
||||
createCurrentUser,
|
||||
createMessage,
|
||||
} from '../../util/test-data';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { BreakdownMaybe } from './TransactionPanel.helpers';
|
||||
import { TransactionPanelComponent } from './TransactionPanel';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
const { Money } = types;
|
||||
|
||||
describe('TransactionPanel - Sale', () => {
|
||||
const baseTxAttrs = {
|
||||
total: new Money(16500, 'USD'),
|
||||
commission: new Money(1000, 'USD'),
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
customer: createUser('customer1'),
|
||||
lastTransitionedAt: new Date(Date.UTC(2017, 5, 10)),
|
||||
};
|
||||
const txPreauthorized = createTransaction({
|
||||
id: 'sale-preauthorized',
|
||||
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txAccepted = createTransaction({
|
||||
id: 'sale-accepted',
|
||||
lastTransition: propTypes.TX_TRANSITION_ACCEPT,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txDeclined = createTransaction({
|
||||
id: 'sale-declined',
|
||||
lastTransition: propTypes.TX_TRANSITION_DECLINE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txAutoDeclined = createTransaction({
|
||||
id: 'sale-autodeclined',
|
||||
lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txCanceled = createTransaction({
|
||||
id: 'sale-canceled',
|
||||
lastTransition: propTypes.TX_TRANSITION_CANCELED,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txDelivered = createTransaction({
|
||||
id: 'sale-delivered',
|
||||
lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const panelBaseProps = {
|
||||
onAcceptSale: noop,
|
||||
onDeclineSale: noop,
|
||||
acceptInProgress: false,
|
||||
declineInProgress: false,
|
||||
currentUser: createCurrentUser('user1'),
|
||||
totalMessages: 2,
|
||||
totalMessagePages: 1,
|
||||
oldestMessagePageFetched: 1,
|
||||
messages: [
|
||||
createMessage('msg1', {}, { sender: createUser('user1') }),
|
||||
createMessage('msg2', {}, { sender: createUser('user2') }),
|
||||
],
|
||||
initialMessageFailed: false,
|
||||
fetchMessagesInProgress: false,
|
||||
sendMessageInProgress: false,
|
||||
sendReviewInProgress: false,
|
||||
onManageDisableScrolling: noop,
|
||||
onOpenReviewModal: noop,
|
||||
onShowMoreMessages: noop,
|
||||
onSendMessage: noop,
|
||||
onSendReview: noop,
|
||||
onResetForm: noop,
|
||||
intl: fakeIntl,
|
||||
};
|
||||
|
||||
it('preauthorized matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txPreauthorized,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('accepted matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txAccepted,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('declined matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txDeclined,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('autodeclined matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txAutoDeclined,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('canceled matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txCanceled,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('delivered matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txDelivered,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders correct total price', () => {
|
||||
const transaction = createTransaction({
|
||||
id: 'sale-tx',
|
||||
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
total: new Money(16500, 'USD'),
|
||||
commission: new Money(1000, 'USD'),
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
customer: createUser('customer1'),
|
||||
lastTransitionedAt: new Date(Date.UTC(2017, 5, 10)),
|
||||
});
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction,
|
||||
};
|
||||
const panel = shallow(<TransactionPanelComponent {...props} />);
|
||||
const breakdownProps = panel
|
||||
.find(BreakdownMaybe)
|
||||
.first()
|
||||
.props();
|
||||
|
||||
// Total price for the provider should be transaction total minus the commission.
|
||||
expect(breakdownProps.transaction.attributes.payoutTotal).toEqual(new Money(15500, 'USD'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('TransactionPanel - Order', () => {
|
||||
const baseTxAttrs = {
|
||||
total: new Money(16500, 'USD'),
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
provider: createUser('provider'),
|
||||
customer: createUser('customer'),
|
||||
};
|
||||
|
||||
const txPreauthorized = createTransaction({
|
||||
id: 'order-preauthorized',
|
||||
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txAccepted = createTransaction({
|
||||
id: 'order-accepted',
|
||||
lastTransition: propTypes.TX_TRANSITION_ACCEPT,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txDeclined = createTransaction({
|
||||
id: 'order-declined',
|
||||
lastTransition: propTypes.TX_TRANSITION_DECLINE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txAutoDeclined = createTransaction({
|
||||
id: 'order-autodeclined',
|
||||
lastTransition: propTypes.TX_TRANSITION_AUTO_DECLINE,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txCanceled = createTransaction({
|
||||
id: 'order-canceled',
|
||||
lastTransition: propTypes.TX_TRANSITION_CANCELED,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const txDelivered = createTransaction({
|
||||
id: 'order-delivered',
|
||||
lastTransition: propTypes.TX_TRANSITION_MARK_DELIVERED,
|
||||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const panelBaseProps = {
|
||||
intl: fakeIntl,
|
||||
currentUser: createCurrentUser('user2'),
|
||||
totalMessages: 2,
|
||||
totalMessagePages: 1,
|
||||
oldestMessagePageFetched: 1,
|
||||
messages: [
|
||||
createMessage('msg1', {}, { sender: createUser('user1') }),
|
||||
createMessage('msg2', {}, { sender: createUser('user2') }),
|
||||
],
|
||||
initialMessageFailed: false,
|
||||
fetchMessagesInProgress: false,
|
||||
sendMessageInProgress: false,
|
||||
sendReviewInProgress: false,
|
||||
onManageDisableScrolling: noop,
|
||||
onOpenReviewModal: noop,
|
||||
onShowMoreMessages: noop,
|
||||
onSendMessage: noop,
|
||||
onSendReview: noop,
|
||||
onResetForm: noop,
|
||||
onAcceptSale: noop,
|
||||
onDeclineSale: noop,
|
||||
acceptInProgress: false,
|
||||
declineInProgress: false,
|
||||
};
|
||||
|
||||
it('preauthorized matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txPreauthorized,
|
||||
};
|
||||
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('accepted matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txAccepted,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('declined matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txDeclined,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('autodeclined matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txAutoDeclined,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('canceled matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txCanceled,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('delivered matches snapshot', () => {
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: txDelivered,
|
||||
};
|
||||
const tree = renderShallow(<TransactionPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders correct total price', () => {
|
||||
const { Money } = types;
|
||||
const tx = createTransaction({
|
||||
id: 'order-tx',
|
||||
lastTransition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
total: new Money(16500, 'USD'),
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
provider: createUser('provider'),
|
||||
customer: createUser('customer'),
|
||||
});
|
||||
const props = {
|
||||
...panelBaseProps,
|
||||
transaction: tx,
|
||||
};
|
||||
const panel = shallow(<TransactionPanelComponent {...props} />);
|
||||
const breakdownProps = panel
|
||||
.find(BreakdownMaybe)
|
||||
.first()
|
||||
.props();
|
||||
expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD'));
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -76,7 +76,6 @@ export { default as ModalInMobile } from './ModalInMobile/ModalInMobile';
|
|||
export { default as NamedLink } from './NamedLink/NamedLink';
|
||||
export { default as NamedRedirect } from './NamedRedirect/NamedRedirect';
|
||||
export { default as NotificationBadge } from './NotificationBadge/NotificationBadge';
|
||||
export { default as OrderDetailsPanel } from './OrderDetailsPanel/OrderDetailsPanel';
|
||||
export { default as OrderDiscussionPanel } from './OrderDiscussionPanel/OrderDiscussionPanel';
|
||||
export { default as Page } from './Page/Page';
|
||||
export { default as PaginationLinks } from './PaginationLinks/PaginationLinks';
|
||||
|
|
@ -86,7 +85,6 @@ export { default as ResponsiveImage } from './ResponsiveImage/ResponsiveImage';
|
|||
export { default as ReviewModal } from './ReviewModal/ReviewModal';
|
||||
export { default as ReviewRating } from './ReviewRating/ReviewRating';
|
||||
export { default as Reviews } from './Reviews/Reviews';
|
||||
export { default as SaleDetailsPanel } from './SaleDetailsPanel/SaleDetailsPanel';
|
||||
export { default as SearchMap } from './SearchMap/SearchMap';
|
||||
export { default as SearchMapGroupLabel } from './SearchMapGroupLabel/SearchMapGroupLabel';
|
||||
export { default as SearchMapInfoCard } from './SearchMapInfoCard/SearchMapInfoCard';
|
||||
|
|
@ -107,6 +105,7 @@ export { default as TextInputField } from './TextInputField/TextInputField';
|
|||
export { default as Topbar } from './Topbar/Topbar';
|
||||
export { default as TopbarDesktop } from './TopbarDesktop/TopbarDesktop';
|
||||
export { default as TopbarMobileMenu } from './TopbarMobileMenu/TopbarMobileMenu';
|
||||
export { default as TransactionPanel } from './TransactionPanel/TransactionPanel';
|
||||
export { default as UserCard } from './UserCard/UserCard';
|
||||
export { default as UserNav } from './UserNav/UserNav';
|
||||
export { default as ValidationError } from './ValidationError/ValidationError';
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@
|
|||
margin: 0;
|
||||
max-height: 183px;
|
||||
|
||||
/* We need to remove horizontal paddings,
|
||||
since textarea uses content-box (autosize library fix). */
|
||||
width: calc(100vw - 100px);
|
||||
|
||||
background-color: var(--matterColorLight);
|
||||
|
||||
@media (--viewportMedium) {
|
||||
|
|
@ -32,6 +36,7 @@
|
|||
padding: 0 0 5px 0;
|
||||
margin: 0;
|
||||
max-height: initial;
|
||||
width: 100%;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: var(--attentionColor);
|
||||
background-color: transparent;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
|
|||
import { isScrollingDisabled, manageDisableScrolling } from '../../ducks/UI.duck';
|
||||
import {
|
||||
NamedRedirect,
|
||||
OrderDetailsPanel,
|
||||
SaleDetailsPanel,
|
||||
TransactionPanel,
|
||||
Page,
|
||||
LayoutSingleColumn,
|
||||
LayoutWrapperTopbar,
|
||||
|
|
@ -71,7 +70,12 @@ export const TransactionPageComponent = props => {
|
|||
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const listingTitle = currentListing.attributes.title;
|
||||
const deletedListingTitle = intl.formatMessage({
|
||||
id: 'TransactionPage.deletedListing',
|
||||
});
|
||||
const listingTitle = currentListing.attributes.deleted
|
||||
? deletedListingTitle
|
||||
: currentListing.attributes.title;
|
||||
|
||||
// Redirect users with someone else's direct link to their own inbox/sales or inbox/orders page.
|
||||
const isDataAvailable =
|
||||
|
|
@ -122,42 +126,16 @@ export const TransactionPageComponent = props => {
|
|||
</p>
|
||||
);
|
||||
|
||||
const salePanel = isDataAvailable ? (
|
||||
<SaleDetailsPanel
|
||||
className={detailsClassName}
|
||||
currentUser={currentUser}
|
||||
transaction={currentTransaction}
|
||||
onAcceptSale={onAcceptSale}
|
||||
onDeclineSale={onDeclineSale}
|
||||
acceptInProgress={acceptInProgress}
|
||||
declineInProgress={declineInProgress}
|
||||
acceptSaleError={acceptSaleError}
|
||||
declineSaleError={declineSaleError}
|
||||
totalMessagePages={totalMessagePages}
|
||||
oldestMessagePageFetched={oldestMessagePageFetched}
|
||||
messages={messages}
|
||||
fetchMessagesInProgress={fetchMessagesInProgress}
|
||||
fetchMessagesError={fetchMessagesError}
|
||||
sendMessageInProgress={sendMessageInProgress}
|
||||
sendMessageError={sendMessageError}
|
||||
sendReviewInProgress={sendReviewInProgress}
|
||||
sendReviewError={sendReviewError}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
onShowMoreMessages={onShowMoreMessages}
|
||||
onSendMessage={onSendMessage}
|
||||
onSendReview={onSendReview}
|
||||
onResetForm={onResetForm}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const initialMessageFailed = !!(
|
||||
initialMessageFailedToTransaction &&
|
||||
currentTransaction.id &&
|
||||
initialMessageFailedToTransaction.uuid === currentTransaction.id.uuid
|
||||
);
|
||||
|
||||
const orderPanel = isDataAvailable ? (
|
||||
<OrderDetailsPanel
|
||||
// TransactionPanel is presentational component
|
||||
// that currently handles showing everything inside layout's main view area.
|
||||
const panel = isDataAvailable ? (
|
||||
<TransactionPanel
|
||||
className={detailsClassName}
|
||||
currentUser={currentUser}
|
||||
transaction={currentTransaction}
|
||||
|
|
@ -176,11 +154,17 @@ export const TransactionPageComponent = props => {
|
|||
onSendMessage={onSendMessage}
|
||||
onSendReview={onSendReview}
|
||||
onResetForm={onResetForm}
|
||||
transactionRole={transactionRole}
|
||||
onAcceptSale={onAcceptSale}
|
||||
onDeclineSale={onDeclineSale}
|
||||
acceptInProgress={acceptInProgress}
|
||||
declineInProgress={declineInProgress}
|
||||
acceptSaleError={acceptSaleError}
|
||||
declineSaleError={declineSaleError}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const saleOrOrderPanel = isDataAvailable && isOwnSale ? salePanel : orderPanel;
|
||||
const panel = isDataAvailable ? saleOrOrderPanel : loadingOrFailedFetching;
|
||||
) : (
|
||||
loadingOrFailedFetching
|
||||
);
|
||||
|
||||
return (
|
||||
<Page
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
|
|||
rootClassName={null}
|
||||
>
|
||||
<div>
|
||||
<InjectIntl(OrderDetailsPanelComponent)
|
||||
<InjectIntl(TransactionPanelComponent)
|
||||
acceptInProgress={false}
|
||||
acceptSaleError={null}
|
||||
className=""
|
||||
currentUser={
|
||||
Object {
|
||||
|
|
@ -42,11 +44,15 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
|
|||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
declineInProgress={false}
|
||||
declineSaleError={null}
|
||||
fetchMessagesError={null}
|
||||
fetchMessagesInProgress={false}
|
||||
initialMessageFailed={false}
|
||||
messages={Array []}
|
||||
oldestMessagePageFetched={0}
|
||||
onAcceptSale={[Function]}
|
||||
onDeclineSale={[Function]}
|
||||
onResetForm={[Function]}
|
||||
onSendMessage={[Function]}
|
||||
onShowMoreMessages={[Function]}
|
||||
|
|
@ -171,6 +177,7 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
|
|||
"type": "transaction",
|
||||
}
|
||||
}
|
||||
transactionRole="customer"
|
||||
/>
|
||||
</div>
|
||||
</LayoutWrapperMain>
|
||||
|
|
@ -204,7 +211,7 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
|
|||
rootClassName={null}
|
||||
>
|
||||
<div>
|
||||
<InjectIntl(SaleDetailsPanelComponent)
|
||||
<InjectIntl(TransactionPanelComponent)
|
||||
acceptInProgress={false}
|
||||
acceptSaleError={null}
|
||||
className=""
|
||||
|
|
@ -231,6 +238,7 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
|
|||
declineInProgress={false}
|
||||
declineSaleError={null}
|
||||
fetchMessagesError={null}
|
||||
initialMessageFailed={false}
|
||||
messages={Array []}
|
||||
oldestMessagePageFetched={0}
|
||||
onAcceptSale={[Function]}
|
||||
|
|
@ -359,6 +367,7 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
|
|||
"type": "transaction",
|
||||
}
|
||||
}
|
||||
transactionRole="provider"
|
||||
/>
|
||||
</div>
|
||||
</LayoutWrapperMain>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"ActivityFeed.bannedUserDisplayName": "Banned user",
|
||||
"ActivityFeed.deletedListing": "deleted listing",
|
||||
"ActivityFeed.leaveAReview": "Leave a review for { displayName }.",
|
||||
"ActivityFeed.leaveAReviewSecond": "Leave a review for { displayName } to see their review.",
|
||||
"ActivityFeed.ownTransitionAccept": "You accepted the booking request.",
|
||||
|
|
@ -284,24 +286,6 @@
|
|||
"NotFoundPage.description": "We can't find the page or sauna you're looking for. Make sure you've typed in the URL correctly or try searching Saunatime.",
|
||||
"NotFoundPage.heading": "Sorry, we couldn't find that page.",
|
||||
"NotFoundPage.title": "Page not found",
|
||||
"OrderDetailsPanel.activityHeading": "Activity",
|
||||
"OrderDetailsPanel.bannedUserDisplayName": "Banned user",
|
||||
"OrderDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
|
||||
"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.messageLoadingFailed": "Something went wrong when loading messages. Please refresh the page and try again.",
|
||||
"OrderDetailsPanel.orderAcceptedSubtitle": "Your booking for {listingLink} has been accepted.",
|
||||
"OrderDetailsPanel.orderAcceptedTitle": "Woohoo {customerName}!",
|
||||
"OrderDetailsPanel.orderCancelledTitle": "{customerName}, your booking for {listingLink} has been cancelled.",
|
||||
"OrderDetailsPanel.orderDeclinedTitle": "{customerName}, your booking for {listingLink} has been declined.",
|
||||
"OrderDetailsPanel.orderDeliveredTitle": "{customerName}, your booking for {listingLink} has been completed.",
|
||||
"OrderDetailsPanel.orderPreauthorizedInfo": "{providerName} has been notified about the booking request. Sit back and relax.",
|
||||
"OrderDetailsPanel.orderPreauthorizedSubtitle": "You have requested to book {listingLink}.",
|
||||
"OrderDetailsPanel.orderPreauthorizedTitle": "Great success, {customerName}!",
|
||||
"OrderDetailsPanel.sendMessagePlaceholder": "Send a message to {name}…",
|
||||
"Page.schemaDescription": "Book a sauna using Saunatime or earn some income by sharing your sauna",
|
||||
"Page.schemaTitle": "Book saunas everywhere | {siteTitle}",
|
||||
"PaginationLinks.next": "Next page",
|
||||
|
|
@ -458,22 +442,6 @@
|
|||
"ReviewModal.description": "Reviews are an important part of the Saunatime community. Please share what went well and what could have been improved.",
|
||||
"ReviewModal.later": "Later",
|
||||
"ReviewModal.title": "Leave a review for {revieweeName}",
|
||||
"SaleDetailsPanel.acceptButton": "Accept",
|
||||
"SaleDetailsPanel.acceptSaleFailed": "Oops, accepting failed. Please try again.",
|
||||
"SaleDetailsPanel.activityHeading": "Activity",
|
||||
"SaleDetailsPanel.bannedUserDisplayName": "Banned user",
|
||||
"SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
|
||||
"SaleDetailsPanel.customerBannedStatus": "The user made the request, but was later banned.",
|
||||
"SaleDetailsPanel.declineButton": "Decline",
|
||||
"SaleDetailsPanel.declineSaleFailed": "Oops, declining failed. Please try again.",
|
||||
"SaleDetailsPanel.messageLoadingFailed": "Something went wrong when loading messages. Please refresh the page and try again.",
|
||||
"SaleDetailsPanel.saleAcceptedTitle": "You accepted a request from {customerName} to book {listingLink}.",
|
||||
"SaleDetailsPanel.saleCancelledTitle": "The booking from {customerName} for {listingLink} has been cancelled.",
|
||||
"SaleDetailsPanel.saleDeclinedTitle": "The request from {customerName} to book {listingLink} has been declined.",
|
||||
"SaleDetailsPanel.saleDeliveredTitle": "The booking from {customerName} for {listingLink} has been completed.",
|
||||
"SaleDetailsPanel.saleRequestedInfo": "{customerName} is waiting for your response.",
|
||||
"SaleDetailsPanel.saleRequestedTitle": "{customerName} has requested to book {listingLink}.",
|
||||
"SaleDetailsPanel.sendMessagePlaceholder": "Send a message to {name}…",
|
||||
"SearchMapInfoCard.noImage": "No image",
|
||||
"SearchPage.foundResults": "{count, number} {count, plural, one {sauna} other {saunas}} found",
|
||||
"SearchPage.foundResultsMobile": "{count, number} {count, plural, one {result} other {results}}",
|
||||
|
|
@ -621,11 +589,41 @@
|
|||
"TopbarMobileMenu.yourListingsLink": "Your listings",
|
||||
"TopbarSearchForm.placeholder": "Search saunas…",
|
||||
"TopbarSearchForm.searchHelp": "Tip: You can also search saunas by zip code, for example \"00500\" or city district \"Sörnäinen\".",
|
||||
"TransactionPage.deletedListing": "deleted listing",
|
||||
"TransactionPage.fetchOrderFailed": "Fetching order data failed.",
|
||||
"TransactionPage.fetchSaleFailed": "Fetching sale data failed.",
|
||||
"TransactionPage.loadingOrderData": "Loading order data.",
|
||||
"TransactionPage.loadingSaleData": "Loading sale data.",
|
||||
"TransactionPage.title": "Sale details: {title}",
|
||||
"TransactionPanel.acceptButton": "Accept",
|
||||
"TransactionPanel.acceptSaleFailed": "Oops, accepting failed. Please try again.",
|
||||
"TransactionPanel.activityHeading": "Activity",
|
||||
"TransactionPanel.bannedUserDisplayName": "Banned user",
|
||||
"TransactionPanel.bookingBreakdownTitle": "Booking breakdown",
|
||||
"TransactionPanel.customerBannedStatus": "The user made the request, but was later banned.",
|
||||
"TransactionPanel.declineButton": "Decline",
|
||||
"TransactionPanel.declineSaleFailed": "Oops, declining failed. Please try again.",
|
||||
"TransactionPanel.deletedListingOrderTitle": "a listing",
|
||||
"TransactionPanel.deletedListingTitle": "Deleted listing",
|
||||
"TransactionPanel.hostedBy": "Hosted by {name}",
|
||||
"TransactionPanel.initialMessageFailed": "Whoops, failed to send message from checkout.",
|
||||
"TransactionPanel.messageDeletedListing": "However, the listing is deleted and cannot be viewed anymore.",
|
||||
"TransactionPanel.messageLoadingFailed": "Something went wrong when loading messages. Please refresh the page and try again.",
|
||||
"TransactionPanel.orderAcceptedSubtitle": "Your booking for {listingLink} has been accepted.",
|
||||
"TransactionPanel.orderAcceptedTitle": "Woohoo {customerName}!",
|
||||
"TransactionPanel.orderCancelledTitle": "{customerName}, your booking for {listingLink} has been cancelled.",
|
||||
"TransactionPanel.orderDeclinedTitle": "{customerName}, your booking for {listingLink} has been declined.",
|
||||
"TransactionPanel.orderDeliveredTitle": "{customerName}, your booking for {listingLink} has been completed.",
|
||||
"TransactionPanel.orderPreauthorizedInfo": "{providerName} has been notified about the booking request. Sit back and relax.",
|
||||
"TransactionPanel.orderPreauthorizedSubtitle": "You have requested to book {listingLink}.",
|
||||
"TransactionPanel.orderPreauthorizedTitle": "Great success, {customerName}!",
|
||||
"TransactionPanel.saleAcceptedTitle": "You accepted a request from {customerName} to book {listingLink}.",
|
||||
"TransactionPanel.saleCancelledTitle": "The booking from {customerName} for {listingLink} has been cancelled.",
|
||||
"TransactionPanel.saleDeclinedTitle": "The request from {customerName} to book {listingLink} has been declined.",
|
||||
"TransactionPanel.saleDeliveredTitle": "The booking from {customerName} for {listingLink} has been completed.",
|
||||
"TransactionPanel.saleRequestedInfo": "{customerName} is waiting for your response.",
|
||||
"TransactionPanel.saleRequestedTitle": "{customerName} has requested to book {listingLink}.",
|
||||
"TransactionPanel.sendMessagePlaceholder": "Send a message to {name}…",
|
||||
"UserCard.contactUser": "Contact",
|
||||
"UserCard.heading": "Hello, I'm {name}.",
|
||||
"UserCard.showFullBioLink": "more",
|
||||
|
|
|
|||
|
|
@ -17,13 +17,23 @@
|
|||
* defined. This way we get the validation errors only in the most
|
||||
* specific place and avoid duplicate errros.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
instanceOf,
|
||||
number,
|
||||
object,
|
||||
oneOf,
|
||||
oneOfType,
|
||||
shape,
|
||||
string,
|
||||
} from 'prop-types';
|
||||
import Decimal from 'decimal.js';
|
||||
import { types as sdkTypes } from './sdkLoader';
|
||||
import { ensureTransaction } from './data';
|
||||
|
||||
const { UUID, LatLng, LatLngBounds, Money } = sdkTypes;
|
||||
const { arrayOf, bool, func, instanceOf, number, object, oneOf, shape, string } = PropTypes;
|
||||
|
||||
// Fixed value
|
||||
export const value = val => oneOf([val]);
|
||||
|
|
@ -113,19 +123,26 @@ export const user = shape({
|
|||
profileImage: image,
|
||||
});
|
||||
|
||||
const listingAttributes = shape({
|
||||
title: string.isRequired,
|
||||
description: string.isRequired,
|
||||
address: string.isRequired,
|
||||
geolocation: latlng.isRequired,
|
||||
closed: bool.isRequired,
|
||||
deleted: value(false).isRequired,
|
||||
price: money,
|
||||
});
|
||||
|
||||
const deletedListingAttributes = shape({
|
||||
closed: bool.isRequired,
|
||||
deleted: value(true).isRequired,
|
||||
});
|
||||
|
||||
// Denormalised listing object
|
||||
export const listing = shape({
|
||||
id: uuid.isRequired,
|
||||
type: value('listing').isRequired,
|
||||
attributes: shape({
|
||||
title: string.isRequired,
|
||||
description: string.isRequired,
|
||||
address: string.isRequired,
|
||||
geolocation: latlng.isRequired,
|
||||
closed: bool.isRequired,
|
||||
deleted: bool.isRequired,
|
||||
price: money,
|
||||
}).isRequired,
|
||||
attributes: oneOfType([listingAttributes, deletedListingAttributes]).isRequired,
|
||||
author: user,
|
||||
images: arrayOf(image),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue