OrderPage can show review modal (and related changes)

This commit is contained in:
Vesa Luusua 2017-11-29 15:12:10 +02:00
parent 7c8345eec6
commit e954e3271d
5 changed files with 242 additions and 22 deletions

View file

@ -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 {
</div>
</div>
</div>
<ReviewModal
id="ReviewOrderModal"
isOpen={this.state.isReviewModalOpen}
onCloseModal={() => this.setState({ isReviewModalOpen: false })}
onManageDisableScrolling={onManageDisableScrolling}
onSubmitReview={this.onSubmitReview}
revieweeName={authorDisplayName}
reviewSent={this.state.reviewSubmitted}
sendReviewInProgress={sendReviewInProgress}
sendReviewError={sendReviewError}
/>
</div>
);
}
@ -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

View file

@ -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,
};

View file

@ -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`] = `
</div>
</div>
</div>
<InjectIntl(ReviewModal)
id="ReviewOrderModal"
isOpen={false}
onCloseModal={[Function]}
onManageDisableScrolling={[Function]}
onSubmitReview={[Function]}
reviewSent={false}
revieweeName="provider display name"
sendReviewError={null}
sendReviewInProgress={false}
/>
</div>
`;
@ -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`] = `
</div>
</div>
</div>
<InjectIntl(ReviewModal)
id="ReviewOrderModal"
isOpen={false}
onCloseModal={[Function]}
onManageDisableScrolling={[Function]}
onSubmitReview={[Function]}
reviewSent={false}
revieweeName="provider display name"
sendReviewError={null}
sendReviewInProgress={false}
/>
</div>
`;
@ -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`] = `
</div>
</div>
</div>
<InjectIntl(ReviewModal)
id="ReviewOrderModal"
isOpen={false}
onCloseModal={[Function]}
onManageDisableScrolling={[Function]}
onSubmitReview={[Function]}
reviewSent={false}
revieweeName="provider display name"
sendReviewError={null}
sendReviewInProgress={false}
/>
</div>
`;
@ -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`] = `
</div>
</div>
</div>
<InjectIntl(ReviewModal)
id="ReviewOrderModal"
isOpen={false}
onCloseModal={[Function]}
onManageDisableScrolling={[Function]}
onSubmitReview={[Function]}
reviewSent={false}
revieweeName="provider display name"
sendReviewError={null}
sendReviewInProgress={false}
/>
</div>
`;
@ -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`] = `
</div>
</div>
</div>
<InjectIntl(ReviewModal)
id="ReviewOrderModal"
isOpen={false}
onCloseModal={[Function]}
onManageDisableScrolling={[Function]}
onSubmitReview={[Function]}
reviewSent={false}
revieweeName="provider display name"
sendReviewError={null}
sendReviewInProgress={false}
/>
</div>
`;
@ -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`] = `
</div>
</div>
</div>
<InjectIntl(ReviewModal)
id="ReviewOrderModal"
isOpen={false}
onCloseModal={[Function]}
onManageDisableScrolling={[Function]}
onSubmitReview={[Function]}
reviewSent={false}
revieweeName="provider display name"
sendReviewError={null}
sendReviewInProgress={false}
/>
</div>
`;

View file

@ -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)),
]);
};

View file

@ -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)),
});