diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js index 7df809c3..5ce36a93 100644 --- a/src/components/ActivityFeed/ActivityFeed.js +++ b/src/components/ActivityFeed/ActivityFeed.js @@ -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, diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.css b/src/components/OrderDetailsPanel/OrderDetailsPanel.css deleted file mode 100644 index 8b7e84a2..00000000 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.css +++ /dev/null @@ -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; - } -} diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js deleted file mode 100644 index 47f0bf4b..00000000 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js +++ /dev/null @@ -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 ? ( - - ) : null; -}; - -const orderTitle = (transaction, listingLink, customerName) => { - if (propTypes.txIsPreauthorized(transaction)) { - return ( - - - - - - - ); - } else if (propTypes.txIsAccepted(transaction)) { - return ( - - - - - - - ); - } else if (propTypes.txIsDeclined(transaction)) { - return ( - - ); - } else if (propTypes.txIsAutodeclined(transaction)) { - return ( - - ); - } else if (propTypes.txIsCanceled(transaction)) { - return ( - - ); - } else if ( - propTypes.txIsDelivered(transaction) || - propTypes.txHasFirstReview(transaction) || - propTypes.txIsReviewed(transaction) - ) { - return ( - - ); - } else { - return null; - } -}; - -const orderMessage = (transaction, providerName) => { - if (propTypes.txIsPreauthorized(transaction)) { - return ( - - ); - } - 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 = ( - - {title} - - ); - } 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 ? ( -
-

- -

- {initialMessageFailed ? ( -

- -

- ) : null} - {fetchMessagesError ? ( -

- -

- ) : null} - -
- ) : 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 ( -
-
-
-
-
- -
-
-
- -
-

{orderHeading}

- {showInfoMessage ?

{orderInfoText}

: null} - {showInfoMessage ?
: null} -
-

- -

- {bookingInfo} -
- {feedContainer} - -
-
-
-
-
- -
-
-
- -
-
-

{listingTitle}

-

- -

-
-

- -

- {bookingInfo} -
-
-
- this.setState({ isReviewModalOpen: false })} - onManageDisableScrolling={onManageDisableScrolling} - onSubmitReview={this.onSubmitReview} - revieweeName={authorDisplayName} - reviewSent={this.state.reviewSubmitted} - sendReviewInProgress={sendReviewInProgress} - sendReviewError={sendReviewError} - /> -
- ); - } -} - -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; diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js deleted file mode 100644 index 54f7f87d..00000000 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js +++ /dev/null @@ -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( - - ); - expect(tree).toMatchSnapshot(); - }); - it('accepted matches snapshot', () => { - const tree = renderShallow( - - ); - expect(tree).toMatchSnapshot(); - }); - it('declined matches snapshot', () => { - const tree = renderShallow( - - ); - expect(tree).toMatchSnapshot(); - }); - it('autodeclined matches snapshot', () => { - const tree = renderShallow( - - ); - expect(tree).toMatchSnapshot(); - }); - it('canceled matches snapshot', () => { - const tree = renderShallow( - - ); - expect(tree).toMatchSnapshot(); - }); - it('delivered matches snapshot', () => { - const tree = renderShallow( - - ); - 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(); - const breakdownProps = panel.find(BookingBreakdown).props(); - expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD')); - }); -}); diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap deleted file mode 100644 index ba14c0f9..00000000 --- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap +++ /dev/null @@ -1,3933 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-

- - - - - - listing1 title - , - } - } - /> - -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
- -
-
-
- -
-
-

- listing1 title -

-

- -

-
-

- -

- -
-
-
- -
-`; - -exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-

- - listing1 title - , - } - } - /> -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
- -
-
-
- -
-
-

- listing1 title -

-

- -

-
-

- -

- -
-
-
- -
-`; - -exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-

- - - - - - listing1 title - , - } - } - /> - -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
- -
-
-
- -
-
-

- listing1 title -

-

- -

-
-

- -

- -
-
-
- -
-`; - -exports[`OrderDetailsPanel declined matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-

- - listing1 title - , - } - } - /> -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
- -
-
-
- -
-
-

- listing1 title -

-

- -

-
-

- -

- -
-
-
- -
-`; - -exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-

- - listing1 title - , - } - } - /> -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
- -
-
-
- -
-
-

- listing1 title -

-

- -

-
-

- -

- -
-
-
- -
-`; - -exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-

- - - - - - listing1 title - , - } - } - /> - -

-

- -

-
-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
- -
-
-
- -
-
-

- listing1 title -

-

- -

-
-

- -

- -
-
-
- -
-`; diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.js deleted file mode 100644 index a3434e6a..00000000 --- a/src/components/SaleDetailsPanel/SaleDetailsPanel.js +++ /dev/null @@ -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 ? ( - - ) : null; -}; - -const saleTitle = (transaction, listingLink, customerName) => { - if (propTypes.txIsPreauthorized(transaction)) { - return ( - - ); - } else if (propTypes.txIsAccepted(transaction)) { - return ( - - ); - } else if (propTypes.txIsDeclined(transaction)) { - return ( - - ); - } else if (propTypes.txIsAutodeclined(transaction)) { - return ( - - ); - } else if (propTypes.txIsCanceled(transaction)) { - return ( - - ); - } else if ( - propTypes.txIsDelivered(transaction) || - propTypes.txHasFirstReview(transaction) || - propTypes.txIsReviewed(transaction) - ) { - return ( - - ); - } else { - return null; - } -}; - -const saleInfoText = (transaction, customerName) => { - if (propTypes.txIsPreauthorized(transaction)) { - return ; - } - 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 = ( - - {title} - - ); - } - - 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 ? ( -
-

- -

- {fetchMessagesError ? ( -

- -

- ) : null} - -
- ) : null; - - const canShowButtons = propTypes.txIsPreauthorized(currentTransaction) && !isCustomerBanned; - const buttonsDisabled = acceptInProgress || declineInProgress; - - const acceptErrorMessage = acceptSaleError ? ( -

- -

- ) : null; - const declineErrorMessage = declineSaleError ? ( -

- -

- ) : null; - - const actionButtonClasses = classNames(css.actionButtons, { - [css.actionButtonsWithFormFocused]: this.state.sendMessageFormFocused, - }); - const actionButtons = canShowButtons ? ( -
-
- {acceptErrorMessage} - {declineErrorMessage} -
-
- onDeclineSale(currentTransaction.id)} - > - - - onAcceptSale(currentTransaction.id)} - > - - -
-
- ) : 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 ( -
-
-
-
-
- -
-
-
- -
-
- -
-

{title}

- {showInfoText ?

{infoText}

: null} -
-

- -

- {bookingInfo} -
- {feedContainer} - -
{actionButtons}
-
-
-
-
-
- -
-
-

- -

-
{bookingInfo}
-
{actionButtons}
-
-
-
- this.setState({ isReviewModalOpen: false })} - onManageDisableScrolling={onManageDisableScrolling} - onSubmitReview={this.onSubmitReview} - revieweeName={customerDisplayName} - reviewSent={this.state.reviewSubmitted} - sendReviewInProgress={sendReviewInProgress} - sendReviewError={sendReviewError} - /> -
- ); - } -} - -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; diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js deleted file mode 100644 index aa4384ea..00000000 --- a/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js +++ /dev/null @@ -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(); - expect(tree).toMatchSnapshot(); - }); - it('accepted matches snapshot', () => { - const props = { - ...panelBaseProps, - transaction: txAccepted, - }; - const tree = renderShallow(); - expect(tree).toMatchSnapshot(); - }); - it('declined matches snapshot', () => { - const props = { - ...panelBaseProps, - transaction: txDeclined, - }; - const tree = renderShallow(); - expect(tree).toMatchSnapshot(); - }); - it('autodeclined matches snapshot', () => { - const props = { - ...panelBaseProps, - transaction: txAutoDeclined, - }; - const tree = renderShallow(); - expect(tree).toMatchSnapshot(); - }); - it('canceled matches snapshot', () => { - const props = { - ...panelBaseProps, - transaction: txCanceled, - }; - const tree = renderShallow(); - expect(tree).toMatchSnapshot(); - }); - it('delivered matches snapshot', () => { - const props = { - ...panelBaseProps, - transaction: txDelivered, - }; - const tree = renderShallow(); - 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(); - 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')); - }); -}); diff --git a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap deleted file mode 100644 index bc3882e1..00000000 --- a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap +++ /dev/null @@ -1,3661 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SaleDetailsPanel accepted matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-
- -
-

- - listing1 title - , - } - } - /> -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
-
- -
-
-

- -

-
- -
-
-
-
-
- -
-`; - -exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-
- -
-

- - listing1 title - , - } - } - /> -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
-
- -
-
-

- -

-
- -
-
-
-
-
- -
-`; - -exports[`SaleDetailsPanel canceled matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-
- -
-

- - listing1 title - , - } - } - /> -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
-
- -
-
-

- -

-
- -
-
-
-
-
- -
-`; - -exports[`SaleDetailsPanel declined matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-
- -
-

- - listing1 title - , - } - } - /> -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
-
- -
-
-

- -

-
- -
-
-
-
-
- -
-`; - -exports[`SaleDetailsPanel delivered matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-
- -
-

- - listing1 title - , - } - } - /> -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
-
-
- -
-
-

- -

-
- -
-
-
-
-
- -
-`; - -exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` -
-
-
-
-
- -
-
-
- -
-
- -
-

- - listing1 title - , - } - } - /> -

-

- -

-
-

- -

- -
-
-

- -

- -
- -
-
-
-
- - - - - - -
-
-
-
-
-
-
-
- -
-
-

- -

-
- -
-
-
-
-
- - - - - - -
-
-
-
-
-
- -
-`; diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.css b/src/components/TransactionPanel/TransactionPanel.css similarity index 73% rename from src/components/SaleDetailsPanel/SaleDetailsPanel.css rename to src/components/TransactionPanel/TransactionPanel.css index 0aa98539..350189d4 100644 --- a/src/components/SaleDetailsPanel/SaleDetailsPanel.css +++ b/src/components/TransactionPanel/TransactionPanel.css @@ -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; + } +} diff --git a/src/components/TransactionPanel/TransactionPanel.helpers.js b/src/components/TransactionPanel/TransactionPanel.helpers.js new file mode 100644 index 00000000..21abcbc8 --- /dev/null +++ b/src/components/TransactionPanel/TransactionPanel.helpers.js @@ -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 ? ( +
+

+ +

+ {initialMessageFailed ? ( +

+ +

+ ) : null} + {fetchMessagesError ? ( +

+ +

+ ) : null} + { + onShowMoreMessages(currentTransaction.id); + }} + fetchMessagesInProgress={fetchMessagesInProgress} + /> +
+ ) : 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 ? ( +
+

+ +

+ +
+ ) : 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 ? ( +

+ +

+ ) : null; + const declineErrorMessage = declineSaleError ? ( +

+ +

+ ) : null; + + const classes = classNames(rootClassName || css.actionButtons, className); + + return canShowButtons ? ( +
+
+ {acceptErrorMessage} + {declineErrorMessage} +
+
+ onDeclineSale(transaction.id)} + > + + + onAcceptSale(transaction.id)} + > + + +
+
+ ) : null; +}; + +const createListingLink = (listingLoaded, title, id) => { + if (listingLoaded && title) { + const params = { id: id.uuid, slug: createSlug(title) }; + return ( + + {title} + + ); + } else { + return ; + } +}; + +// 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 ( +

+ + + + +

+ ); + } else if (propTypes.txIsAccepted(transaction)) { + return ( +

+ + + + +

+ ); + } else if (propTypes.txIsDeclined(transaction)) { + return ( +

+ +

+ ); + } else if (propTypes.txIsAutodeclined(transaction)) { + return ( +

+ +

+ ); + } else if (propTypes.txIsCanceled(transaction)) { + return ( +

+ +

+ ); + } else if ( + propTypes.txIsDelivered(transaction) || + propTypes.txHasFirstReview(transaction) || + propTypes.txIsReviewed(transaction) + ) { + return ( +

+ +

+ ); + } 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 ( +

+ +

+ ); + } else if (listingDeleted) { + return ( +

+ +

+ ); + } + 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 ( +

+ +

+ ); + } else if (propTypes.txIsAccepted(transaction)) { + return ( +

+ +

+ ); + } else if (propTypes.txIsDeclined(transaction)) { + return ( +

+ +

+ ); + } else if (propTypes.txIsAutodeclined(transaction)) { + return ( +

+ +

+ ); + } else if (propTypes.txIsCanceled(transaction)) { + return ( +

+ +

+ ); + } else if ( + propTypes.txIsDelivered(transaction) || + propTypes.txHasFirstReview(transaction) || + propTypes.txIsReviewed(transaction) + ) { + return ( +

+ +

+ ); + } 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 ( +

+ +

+ ); + } else if (isCustomerBanned) { + return ( +

+ +

+ ); + } + return null; +}; + +// Functional component as a helper to choose and show Order or Sale title +export const TransactionPageTitle = props => { + return props.transactionRole === 'customer' ? ( + + ) : ( + + ); +}; + +// Functional component as a helper to choose and show Order or Sale message +export const TransactionPageMessage = props => { + return props.transactionRole === 'customer' ? ( + + ) : ( + + ); +}; + +// 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, + }; +}; diff --git a/src/components/TransactionPanel/TransactionPanel.js b/src/components/TransactionPanel/TransactionPanel.js new file mode 100644 index 00000000..9a0600c9 --- /dev/null +++ b/src/components/TransactionPanel/TransactionPanel.js @@ -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 = ( + + ); + + 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 ( +
+
+
+
+
+ +
+
+
+ +
+ {isProvider ? ( +
+ +
+ ) : null} + + + + +
+

+ +

+ +
+ + + + + {isProvider && canShowButtons ? ( +
{actionButtons}
+ ) : null} +
+ +
+
+
+
+ +
+
+ {isCustomer ? ( +
+ +
+ ) : null} + {isCustomer ? ( +
+

{listingTitle}

+

+ +

+
+ ) : null} + + {isProvider && canShowButtons ? ( +
{actionButtons}
+ ) : null} +
+
+
+ this.setState({ isReviewModalOpen: false })} + onManageDisableScrolling={onManageDisableScrolling} + onSubmitReview={this.onSubmitReview} + revieweeName={authorDisplayName} + reviewSent={this.state.reviewSubmitted} + sendReviewInProgress={sendReviewInProgress} + sendReviewError={sendReviewError} + /> +
+ ); + } +} + +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; diff --git a/src/components/TransactionPanel/TransactionPanel.test.js b/src/components/TransactionPanel/TransactionPanel.test.js new file mode 100644 index 00000000..d4b7633c --- /dev/null +++ b/src/components/TransactionPanel/TransactionPanel.test.js @@ -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(); + expect(tree).toMatchSnapshot(); + }); + it('accepted matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txAccepted, + }; + const tree = renderShallow(); + expect(tree).toMatchSnapshot(); + }); + it('declined matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txDeclined, + }; + const tree = renderShallow(); + expect(tree).toMatchSnapshot(); + }); + it('autodeclined matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txAutoDeclined, + }; + const tree = renderShallow(); + expect(tree).toMatchSnapshot(); + }); + it('canceled matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txCanceled, + }; + const tree = renderShallow(); + expect(tree).toMatchSnapshot(); + }); + it('delivered matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txDelivered, + }; + const tree = renderShallow(); + 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(); + 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(); + expect(tree).toMatchSnapshot(); + }); + it('accepted matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txAccepted, + }; + const tree = renderShallow(); + expect(tree).toMatchSnapshot(); + }); + it('declined matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txDeclined, + }; + const tree = renderShallow(); + expect(tree).toMatchSnapshot(); + }); + it('autodeclined matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txAutoDeclined, + }; + const tree = renderShallow(); + expect(tree).toMatchSnapshot(); + }); + it('canceled matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txCanceled, + }; + const tree = renderShallow(); + expect(tree).toMatchSnapshot(); + }); + it('delivered matches snapshot', () => { + const props = { + ...panelBaseProps, + transaction: txDelivered, + }; + const tree = renderShallow(); + 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(); + const breakdownProps = panel + .find(BreakdownMaybe) + .first() + .props(); + expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD')); + }); +}); diff --git a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap new file mode 100644 index 00000000..790a8a41 --- /dev/null +++ b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap @@ -0,0 +1,9493 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TransactionPanel - Order accepted matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Order canceled matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Order declined matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Order delivered matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Sale accepted matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Sale canceled matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Sale declined matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Sale delivered matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; + +exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+ + +
+

+ +

+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+`; diff --git a/src/components/index.js b/src/components/index.js index 8cd12c2b..a88c5cad 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -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'; diff --git a/src/containers/SendMessageForm/SendMessageForm.css b/src/containers/SendMessageForm/SendMessageForm.css index ae0bf2ea..b775125d 100644 --- a/src/containers/SendMessageForm/SendMessageForm.css +++ b/src/containers/SendMessageForm/SendMessageForm.css @@ -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; diff --git a/src/containers/TransactionPage/TransactionPage.js b/src/containers/TransactionPage/TransactionPage.js index b1e9db68..460ade0a 100644 --- a/src/containers/TransactionPage/TransactionPage.js +++ b/src/containers/TransactionPage/TransactionPage.js @@ -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 => {

); - const salePanel = isDataAvailable ? ( - - ) : null; - const initialMessageFailed = !!( initialMessageFailedToTransaction && currentTransaction.id && initialMessageFailedToTransaction.uuid === currentTransaction.id.uuid ); - const orderPanel = isDataAvailable ? ( - { 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 (
-
@@ -204,7 +211,7 @@ exports[`TransactionPage - Sale matches snapshot 1`] = ` rootClassName={null} >
-
diff --git a/src/translations/en.json b/src/translations/en.json index 7c5cf3cf..a4540ac1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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", diff --git a/src/util/propTypes.js b/src/util/propTypes.js index 748de3dc..f2c63d2e 100644 --- a/src/util/propTypes.js +++ b/src/util/propTypes.js @@ -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), });