From e954e3271dadb473bd4f03a542e2b80d20c9dec5 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 29 Nov 2017 15:12:10 +0200 Subject: [PATCH] OrderPage can show review modal (and related changes) --- .../OrderDetailsPanel/OrderDetailsPanel.js | 48 +++++++- .../OrderDetailsPanel.test.js | 4 + .../OrderDetailsPanel.test.js.snap | 72 +++++++++++ src/containers/OrderPage/OrderPage.duck.js | 114 +++++++++++++++--- src/containers/OrderPage/OrderPage.js | 26 +++- 5 files changed, 242 insertions(+), 22 deletions(-) diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js index 6d5ce15b..5ba09ee4 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.js @@ -7,11 +7,12 @@ import { createSlug } from '../../util/urlHelpers'; import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data'; import { isMobileSafari } from '../../util/userAgent'; import { + ActivityFeed, + AvatarMedium, BookingBreakdown, NamedLink, ResponsiveImage, - AvatarMedium, - ActivityFeed, + ReviewModal, } from '../../components'; import { SendMessageForm } from '../../containers'; @@ -82,8 +83,29 @@ const orderMessage = (transaction, providerName) => { export class OrderDetailsPanelComponent extends Component { constructor(props) { super(props); - this.state = { sendMessageFormFocused: false }; + 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.id, rating, reviewContent).then(r => + this.setState({ isReviewModalOpen: false, reviewSubmitted: true }) + ); + } + render() { const { rootClassName, @@ -97,6 +119,9 @@ export class OrderDetailsPanelComponent extends Component { fetchMessagesError, sendMessageInProgress, sendMessageError, + sendReviewInProgress, + sendReviewError, + onManageDisableScrolling, onShowMoreMessages, onSendMessage, onResetForm, @@ -200,6 +225,7 @@ export class OrderDetailsPanelComponent extends Component { transaction={currentTransaction} currentUser={currentUser} hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress} + onOpenReviewModal={this.onOpenReviewModal} onShowOlderMessages={handleShowOlderMessages} fetchMessagesInProgress={fetchMessagesInProgress} /> @@ -330,6 +356,17 @@ export class OrderDetailsPanelComponent extends Component { + this.setState({ isReviewModalOpen: false })} + onManageDisableScrolling={onManageDisableScrolling} + onSubmitReview={this.onSubmitReview} + revieweeName={authorDisplayName} + reviewSent={this.state.reviewSubmitted} + sendReviewInProgress={sendReviewInProgress} + sendReviewError={sendReviewError} + /> ); } @@ -341,6 +378,7 @@ OrderDetailsPanelComponent.defaultProps = { currentUser: null, fetchMessagesError: null, sendMessageError: null, + sendReviewError: null, }; const { string, arrayOf, bool, func, number } = PropTypes; @@ -358,8 +396,12 @@ OrderDetailsPanelComponent.propTypes = { 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 diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js index bdb66b16..df68e3c5 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js @@ -78,8 +78,12 @@ describe('OrderDetailsPanel', () => { initialMessageFailed: false, fetchMessagesInProgress: false, sendMessageInProgress: false, + sendReviewInProgress: false, + onManageDisableScrolling: noop, + onOpenReviewModal: noop, onShowMoreMessages: noop, onSendMessage: noop, + onSendReview: noop, onResetForm: noop, }; diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap index 71cf60de..241719cc 100644 --- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap +++ b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap @@ -310,6 +310,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -644,6 +645,17 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` + `; @@ -945,6 +957,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -1279,6 +1292,17 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` + `; @@ -1592,6 +1616,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -1926,6 +1951,17 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` + `; @@ -2227,6 +2263,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -2561,6 +2598,17 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` + `; @@ -2862,6 +2910,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -3196,6 +3245,17 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` + `; @@ -3520,6 +3580,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -3854,5 +3915,16 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` + `; diff --git a/src/containers/OrderPage/OrderPage.duck.js b/src/containers/OrderPage/OrderPage.duck.js index 651091f1..3d5d23c0 100644 --- a/src/containers/OrderPage/OrderPage.duck.js +++ b/src/containers/OrderPage/OrderPage.duck.js @@ -1,7 +1,8 @@ import { pick } from 'lodash'; import { types } from '../../util/sdkLoader'; -import { storableError } from '../../util/errors'; -import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck'; +import { isTransactionsTransitionInvalidTransition, storableError } from '../../util/errors'; +import * as propTypes from '../../util/propTypes'; +import { addMarketplaceEntities, getTransactionsById } from '../../ducks/marketplaceData.duck'; import { updatedEntities, denormalisedEntities } from '../../util/data'; const MESSAGES_PAGE_SIZE = 100; @@ -22,6 +23,10 @@ export const SEND_MESSAGE_REQUEST = 'app/OrderPage/SEND_MESSAGE_REQUEST'; export const SEND_MESSAGE_SUCCESS = 'app/OrderPage/SEND_MESSAGE_SUCCESS'; export const SEND_MESSAGE_ERROR = 'app/OrderPage/SEND_MESSAGE_ERROR'; +export const SEND_REVIEW_REQUEST = 'app/OrderPage/SEND_REVIEW_REQUEST'; +export const SEND_REVIEW_SUCCESS = 'app/OrderPage/SEND_REVIEW_SUCCESS'; +export const SEND_REVIEW_ERROR = 'app/OrderPage/SEND_REVIEW_ERROR'; + // ================ Reducer ================ // const initialState = { @@ -35,6 +40,8 @@ const initialState = { messageSendingFailedToTransaction: null, sendMessageInProgress: false, sendMessageError: null, + sendReviewInProgress: false, + sendReviewError: null, }; export default function checkoutPageReducer(state = initialState, action = {}) { @@ -72,6 +79,13 @@ export default function checkoutPageReducer(state = initialState, action = {}) { case SEND_MESSAGE_ERROR: return { ...state, sendMessageInProgress: false, sendMessageError: payload }; + case SEND_REVIEW_REQUEST: + return { ...state, sendReviewInProgress: true, sendReviewError: null }; + case SEND_REVIEW_SUCCESS: + return { ...state, sendReviewInProgress: false }; + case SEND_REVIEW_ERROR: + return { ...state, sendReviewInProgress: false, sendReviewError: payload }; + default: return state; } @@ -105,6 +119,10 @@ const sendMessageRequest = () => ({ type: SEND_MESSAGE_REQUEST }); const sendMessageSuccess = () => ({ type: SEND_MESSAGE_SUCCESS }); const sendMessageError = e => ({ type: SEND_MESSAGE_ERROR, error: true, payload: e }); +const sendReviewRequest = () => ({ type: SEND_REVIEW_REQUEST }); +const sendReviewSuccess = () => ({ type: SEND_REVIEW_SUCCESS }); +const sendReviewError = e => ({ type: SEND_REVIEW_ERROR, error: true, payload: e }); + // ================ Thunks ================ // const listingRelationship = txResponse => { @@ -200,21 +218,6 @@ export const fetchMoreMessages = txId => (dispatch, getState, sdk) => { return dispatch(fetchNLatestMessages(txId, messagesToFetch)); }; -// loadData is a collection of async calls that need to be made -// before page has all the info it needs to render itself -export const loadData = params => dispatch => { - const orderId = new types.UUID(params.id); - - // Clear the send error since the message form is emptied as well. - dispatch(setInitialValues({ sendMessageError: null })); - - // Order (i.e. transaction entity in API, but from buyers perspective) contains order details - return Promise.all([ - dispatch(fetchOrder(orderId)), - dispatch(fetchNLatestMessages(orderId, MESSAGES_PAGE_SIZE)), - ]); -}; - export const sendMessage = (orderId, message) => (dispatch, getState, sdk) => { dispatch(sendMessageRequest()); @@ -243,3 +246,80 @@ export const sendMessage = (orderId, message) => (dispatch, getState, sdk) => { throw e; }); }; + +// If other party (provider) has already sent a review, we need to make transition to +// TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND +const sendReviewAsSecond = (id, params, dispatch, sdk) => { + const transition = propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND; + return sdk.transactions + .transition({ id, transition, params }, { expand: true }) + .then(response => { + dispatch(addMarketplaceEntities(response)); + dispatch(sendReviewSuccess()); + return response; + }) + .catch(e => { + dispatch(sendReviewError(storableError(e))); + + // Rethrow so the page can track whether the sending failed, and + // keep the message in the form for a retry. + throw e; + }); +}; + +// If other party (provider) has not yet sent a review, we need to make transition to +// TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST +// However, the other party might have made the review after previous data synch point. +// So, error is likely to happen and then we must try another state transition +// by calling sendReviewAsSecond(). +const sendReviewAsFirst = (id, params, dispatch, sdk) => { + const transition = propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST; + return sdk.transactions + .transition({ id, transition, params }, { expand: true }) + .then(response => { + dispatch(addMarketplaceEntities(response)); + dispatch(sendReviewSuccess()); + return response; + }) + .catch(e => { + // If transaction transition is invalid, lets try another endpoint. + if (isTransactionsTransitionInvalidTransition(e)) { + sendReviewAsSecond(id, params, dispatch, sdk); + } else { + dispatch(sendReviewError(storableError(e))); + + // Rethrow so the page can track whether the sending failed, and + // keep the message in the form for a retry. + throw e; + } + }); +}; + +export const sendReview = (orderId, reviewRating, reviewContent) => (dispatch, getState, sdk) => { + const params = { reviewRating, reviewContent }; + const txs = getTransactionsById(getState(), [orderId]); + const txStateProviderFirst = + txs.length === 1 && + txs[0].attributes.lastTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST; + + dispatch(sendReviewRequest()); + + return txStateProviderFirst + ? sendReviewAsSecond(orderId, params, dispatch, sdk) + : sendReviewAsFirst(orderId, params, dispatch, sdk); +}; + +// loadData is a collection of async calls that need to be made +// before page has all the info it needs to render itself +export const loadData = params => dispatch => { + const orderId = new types.UUID(params.id); + + // Clear the send error since the message form is emptied as well. + dispatch(setInitialValues({ sendMessageError: null, sendReviewError: null })); + + // Order (i.e. transaction entity in API, but from buyers perspective) contains order details + return Promise.all([ + dispatch(fetchOrder(orderId)), + dispatch(fetchNLatestMessages(orderId, MESSAGES_PAGE_SIZE)), + ]); +}; diff --git a/src/containers/OrderPage/OrderPage.js b/src/containers/OrderPage/OrderPage.js index 747ff51e..d284ce14 100644 --- a/src/containers/OrderPage/OrderPage.js +++ b/src/containers/OrderPage/OrderPage.js @@ -8,7 +8,7 @@ import { FormattedMessage, intlShape, injectIntl } from 'react-intl'; import * as propTypes from '../../util/propTypes'; import { ensureListing, ensureTransaction } from '../../util/data'; import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck'; -import { isScrollingDisabled } from '../../ducks/UI.duck'; +import { isScrollingDisabled, manageDisableScrolling } from '../../ducks/UI.duck'; import { NamedRedirect, OrderDetailsPanel, @@ -21,7 +21,13 @@ import { } from '../../components'; import { TopbarContainer } from '../../containers'; -import { loadData, setInitialValues, sendMessage, fetchMoreMessages } from './OrderPage.duck'; +import { + loadData, + setInitialValues, + sendMessage, + sendReview, + fetchMoreMessages, +} from './OrderPage.duck'; import css from './OrderPage.css'; // OrderPage handles data loading @@ -37,8 +43,12 @@ export const OrderPageComponent = props => { messageSendingFailedToTransaction, sendMessageInProgress, sendMessageError, + sendReviewInProgress, + sendReviewError, + onManageDisableScrolling, onShowMoreMessages, onSendMessage, + onSendReview, onResetForm, intl, params, @@ -96,8 +106,12 @@ export const OrderPageComponent = props => { fetchMessagesError={fetchMessagesError} sendMessageInProgress={sendMessageInProgress} sendMessageError={sendMessageError} + sendReviewInProgress={sendReviewInProgress} + sendReviewError={sendReviewError} + onManageDisableScrolling={onManageDisableScrolling} onShowMoreMessages={onShowMoreMessages} onSendMessage={onSendMessage} + onSendReview={onSendReview} onResetForm={onResetForm} /> ) : ( @@ -168,6 +182,8 @@ const mapStateToProps = state => { messageSendingFailedToTransaction, sendMessageInProgress, sendMessageError, + sendReviewInProgress, + sendReviewError, } = state.OrderPage; const transactions = getMarketplaceEntities(state, transactionRef ? [transactionRef] : []); const transaction = transactions.length > 0 ? transactions[0] : null; @@ -182,14 +198,20 @@ const mapStateToProps = state => { messageSendingFailedToTransaction, sendMessageInProgress, sendMessageError, + sendReviewInProgress, + sendReviewError, scrollingDisabled: isScrollingDisabled(state), transaction, }; }; const mapDispatchToProps = dispatch => ({ + onManageDisableScrolling: (componentId, disableScrolling) => + dispatch(manageDisableScrolling(componentId, disableScrolling)), onShowMoreMessages: orderId => dispatch(fetchMoreMessages(orderId)), onSendMessage: (orderId, message) => dispatch(sendMessage(orderId, message)), + onSendReview: (orderId, reviewRating, reviewContent) => + dispatch(sendReview(orderId, reviewRating, reviewContent)), onResetForm: formName => dispatch(resetForm(formName)), });