Merge pull request #575 from sharetribe/show-reviews

Show reviews
This commit is contained in:
Hannu Lyytikäinen 2017-11-29 14:13:40 +02:00 committed by GitHub
commit a49a841205
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 467 additions and 49 deletions

View file

@ -62,7 +62,7 @@
box-shadow: var(--boxShadow);
@media (--viewportMedium) {
padding: 8px 14px 9px 14px;
padding: 6px 14px 11px 14px;
margin: 0;
}
}
@ -137,6 +137,32 @@
margin-right: 6px;
}
.reviewContent {
@apply --marketplaceH4FontStyles;
font-style: italic;
white-space: pre-line;
margin: 8px 0 0 0;
@media (--viewportMedium) {
max-width: 500px;
margin: 7px 0 0 0;
}
}
.reviewStars {
margin-top: 4px;
@media (--viewportMedium) {
margin-top: 2px;
}
}
.reviewStar {
width: 12px;
height: 12px;
font-styles: initial;
}
.showOlderWrapper {
text-align: center;
margin-bottom: 1px;

View file

@ -6,6 +6,7 @@ import {
createTransaction,
createListing,
createTxTransition,
createReview,
} from '../../util/test-data';
import * as propTypes from '../../util/propTypes';
import ActivityFeed from './ActivityFeed';
@ -84,7 +85,7 @@ export const WithTransitions = {
group: 'messages',
};
export const WithMessagesAndTransitions = {
export const WithMessagesTransitionsAndReviews = {
component: ActivityFeed,
props: {
currentUser: createCurrentUser('user2'),
@ -92,6 +93,7 @@ export const WithMessagesAndTransitions = {
customer: createUser('user1'),
provider: createUser('user2'),
listing: createListing('Listing'),
lastTransition: propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND,
transitions: [
createTxTransition({
at: new Date(Date.UTC(2017, 10, 9, 8, 10)),
@ -113,6 +115,28 @@ export const WithMessagesAndTransitions = {
by: propTypes.TX_TRANSITION_ACTOR_PROVIDER,
transition: propTypes.TX_TRANSITION_MARK_DELIVERED,
}),
createTxTransition({
at: new Date(Date.UTC(2017, 10, 9, 11, 34)),
by: propTypes.TX_TRANSITION_ACTOR_PROVIDER,
transition: propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST,
}),
createTxTransition({
at: new Date(Date.UTC(2017, 10, 9, 12, 34)),
by: propTypes.TX_TRANSITION_ACTOR_CUSTOMER,
transition: propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND,
}),
],
reviews: [
createReview(
'review1',
{ at: new Date(Date.UTC(2017, 10, 9, 11, 34)), rating: 3, type: 'ofCustomer' },
{ author: createUser('user2'), subject: createUser('user1') }
),
createReview(
'review2',
{ at: new Date(Date.UTC(2017, 10, 9, 12, 34)), rating: 5, type: 'ofProvider' },
{ author: createUser('user1'), subject: createUser('user2') }
),
],
}),
messages: [
@ -144,6 +168,48 @@ export const WithMessagesAndTransitions = {
group: 'messages',
};
export const WithAReviewFromBothUsers = {
component: ActivityFeed,
props: {
currentUser: createCurrentUser('user1'),
transaction: createTransaction({
customer: createUser('user1'),
provider: createUser('user2'),
listing: createListing('Listing'),
reviews: [
createReview(
'review1',
{ at: new Date(Date.UTC(2017, 10, 9, 8, 10)), rating: 3, type: 'ofProvider' },
{ author: createUser('user1'), subject: createUser('user2') }
),
createReview(
'review2',
{ at: new Date(Date.UTC(2017, 10, 10, 8, 10)), rating: 5, type: 'ofCustomer' },
{ author: createUser('user2'), subject: createUser('user1') }
),
],
lastTransition: propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND,
transitions: [
createTxTransition({
at: new Date(Date.UTC(2017, 10, 9, 8, 10)),
by: propTypes.TX_TRANSITION_ACTOR_CUSTOMER,
transition: propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST,
}),
createTxTransition({
at: new Date(Date.UTC(2017, 10, 10, 8, 10)),
by: propTypes.TX_TRANSITION_ACTOR_PROVIDER,
transition: propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND,
}),
],
}),
messages: [],
hasOlderMessages: false,
onShowOlderMessages: noop,
fetchMessagesInProgress: false,
},
group: 'messages',
};
class PagedFeed extends Component {
constructor(props) {
super(props);

View file

@ -1,9 +1,9 @@
import React from 'react';
import { string, arrayOf, bool, func } from 'prop-types';
import { string, arrayOf, bool, func, number } from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import { dropWhile } from 'lodash';
import classNames from 'classnames';
import { Avatar, InlineTextButton } from '../../components';
import { Avatar, InlineTextButton, ReviewRating } from '../../components';
import { formatDate } from '../../util/dates';
import { ensureTransaction, ensureUser, ensureListing } from '../../util/data';
import * as propTypes from '../../util/propTypes';
@ -47,43 +47,144 @@ OwnMessage.propTypes = {
intl: intlShape.isRequired,
};
const Review = props => {
const { content, rating } = props;
return (
<div>
<p className={css.reviewContent}>{content}</p>
<ReviewRating
reviewStarClassName={css.reviewStar}
className={css.reviewStars}
rating={rating}
/>
</div>
);
};
Review.propTypes = {
content: string.isRequired,
rating: number.isRequired,
};
const hasUserLeftAReviewFirst = (userRole, lastTransition) => {
return (
(lastTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST &&
userRole === propTypes.TX_TRANSITION_ACTOR_CUSTOMER) ||
(lastTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST &&
userRole === propTypes.TX_TRANSITION_ACTOR_PROVIDER) ||
propTypes.areReviewsCompleted(lastTransition)
);
};
const onLeaveAReview = () => {
// TODO: Open up a review modal
console.log('Leave a review button clicked');
};
const resolveTransitionMessage = (
transition,
lastTransition,
listingTitle,
ownRole,
otherUsersName,
intl
) => {
const isOwnTransition = transition.by === ownRole;
const currentTransition = transition.transition;
const displayName = otherUsersName;
const deliveredState = lastTransition === propTypes.TX_TRANSITION_MARK_DELIVERED;
switch (currentTransition) {
case propTypes.TX_TRANSITION_PREAUTHORIZE:
return isOwnTransition ? (
<FormattedMessage id="ActivityFeed.ownTransitionRequest" values={{ listingTitle }} />
) : (
<FormattedMessage
id="ActivityFeed.transitionRequest"
values={{ displayName, listingTitle }}
/>
);
case propTypes.TX_TRANSITION_ACCEPT:
return isOwnTransition ? (
<FormattedMessage id="ActivityFeed.ownTransitionAccept" />
) : (
<FormattedMessage id="ActivityFeed.transitionAccept" values={{ displayName }} />
);
case propTypes.TX_TRANSITION_DECLINE:
return isOwnTransition ? (
<FormattedMessage id="ActivityFeed.ownTransitionDecline" />
) : (
<FormattedMessage id="ActivityFeed.transitionDecline" />
);
case propTypes.TX_TRANSITION_AUTO_DECLINE:
return ownRole === propTypes.TX_TRANSITION_ACTOR_PROVIDER ? (
<FormattedMessage id="ActivityFeed.ownTransitionAutoDecline" />
) : (
<FormattedMessage id="ActivityFeed.transitionAutoDecline" values={{ displayName }} />
);
case propTypes.TX_TRANSITION_CANCEL:
return <FormattedMessage id="ActivityFeed.transitionCancel" />;
case propTypes.TX_TRANSITION_MARK_DELIVERED:
// Show the leave a review link if the state is delivered or
// if current user is not the first to leave a review
const reviewLink =
deliveredState || !hasUserLeftAReviewFirst(ownRole, lastTransition) ? (
<InlineTextButton onClick={onLeaveAReview}>
<FormattedMessage id="ActivityFeed.leaveAReview" values={{ displayName }} />
</InlineTextButton>
) : null;
return <FormattedMessage id="ActivityFeed.transitionComplete" values={{ reviewLink }} />;
case propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST:
case propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST:
if (isOwnTransition) {
return <FormattedMessage id="ActivityFeed.ownTransitionReview" values={{ displayName }} />;
} else {
// show the leave a review link if current user is not the first
// one to leave a review
const reviewLink = !hasUserLeftAReviewFirst(ownRole, lastTransition) ? (
<InlineTextButton onClick={onLeaveAReview}>
<FormattedMessage id="ActivityFeed.leaveAReviewSecond" values={{ displayName }} />
</InlineTextButton>
) : null;
return (
<FormattedMessage
id="ActivityFeed.transitionReview"
values={{ displayName, reviewLink }}
/>
);
}
case propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND:
case propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND:
if (isOwnTransition) {
return <FormattedMessage id="ActivityFeed.ownTransitionReview" values={{ displayName }} />;
} else {
return (
<FormattedMessage
id="ActivityFeed.transitionReview"
values={{ displayName, reviewLink: null }}
/>
);
}
default:
return '';
}
};
const reviewByAuthorId = (transaction, userId) => {
return transaction.reviews.filter(r => r.author.id.uuid === userId.uuid)[0];
};
const Transition = props => {
const { transition, currentUser, customer, provider, listingTitle, intl } = props;
const { transition, transaction, currentUser, intl } = props;
const resolveTransitionMessage = (transition, ownRole, otherUsersName, intl) => {
const isOwnTransition = transition.by === ownRole;
const transitionType = transition.transition;
const displayName = otherUsersName;
switch (transitionType) {
case propTypes.TX_TRANSITION_PREAUTHORIZE:
return isOwnTransition
? intl.formatMessage({ id: 'ActivityFeed.ownTransitionRequest' }, { listingTitle })
: intl.formatMessage(
{ id: 'ActivityFeed.transitionRequest' },
{ displayName, listingTitle }
);
case propTypes.TX_TRANSITION_ACCEPT:
return isOwnTransition
? intl.formatMessage({ id: 'ActivityFeed.ownTransitionAccept' })
: intl.formatMessage({ id: 'ActivityFeed.transitionAccept' }, { displayName });
case propTypes.TX_TRANSITION_DECLINE:
return isOwnTransition
? intl.formatMessage({ id: 'ActivityFeed.ownTransitionDecline' })
: intl.formatMessage({ id: 'ActivityFeed.transitionDecline' }, { displayName });
case propTypes.TX_TRANSITION_AUTO_DECLINE:
return ownRole === propTypes.TX_TRANSITION_ACTOR_PROVIDER
? intl.formatMessage({ id: 'ActivityFeed.ownTransitionAutoDecline' })
: intl.formatMessage({ id: 'ActivityFeed.transitionAutoDecline' }, { displayName });
case propTypes.TX_TRANSITION_MARK_DELIVERED:
return intl.formatMessage({ id: 'ActivityFeed.transitionComplete' });
case propTypes.TX_TRANSITION_CANCEL:
return intl.formatMessage({ id: 'ActivityFeed.transitionCancel' });
default:
return '';
}
};
const currentTransaction = ensureTransaction(transaction);
const customer = currentTransaction.customer;
const provider = currentTransaction.provider;
const listingTitle = currentTransaction.listing.attributes.title;
const lastTransition = currentTransaction.attributes.lastTransition;
const ownRole =
currentUser.id.uuid === customer.id.uuid
@ -95,7 +196,41 @@ const Transition = props => {
? provider.attributes.profile.displayName
: customer.attributes.profile.displayName;
const transitionMessage = resolveTransitionMessage(transition, ownRole, otherUsersName, intl);
const transitionMessage = resolveTransitionMessage(
transition,
lastTransition,
listingTitle,
ownRole,
otherUsersName,
intl
);
const currentTransition = transition.transition;
let reviewComponent = null;
if (
propTypes.isReviewTransition(currentTransition) &&
propTypes.areReviewsCompleted(lastTransition)
) {
const customerReview =
currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST ||
currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND;
const providerReview =
currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST ||
currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND;
if (customerReview) {
const review = reviewByAuthorId(currentTransaction, customer.id);
reviewComponent = (
<Review content={review.attributes.content} rating={review.attributes.rating} />
);
} else if (providerReview) {
const review = reviewByAuthorId(currentTransaction, provider.id);
reviewComponent = (
<Review content={review.attributes.content} rating={review.attributes.rating} />
);
}
}
const todayString = intl.formatMessage({ id: 'ActivityFeed.today' });
return (
@ -106,6 +241,7 @@ const Transition = props => {
<div>
<p className={css.transitionContent}>{transitionMessage}</p>
<p className={css.transitionDate}>{formatDate(intl, todayString, transition.at)}</p>
{reviewComponent}
</div>
</div>
);
@ -113,10 +249,8 @@ const Transition = props => {
Transition.propTypes = {
transition: propTypes.txTransition.isRequired,
transaction: propTypes.transaction.isRequired,
currentUser: propTypes.currentUser.isRequired,
customer: propTypes.user.isRequired,
provider: propTypes.user.isRequired,
listingTitle: string.isRequired,
intl: intlShape.isRequired,
};
@ -180,7 +314,8 @@ export const ActivityFeedComponent = props => {
currentUser &&
currentUser.id &&
currentCustomer.id &&
currentProvider.id
currentProvider.id &&
currentListing.id
);
// combine messages and transaction transitions
@ -191,10 +326,8 @@ export const ActivityFeedComponent = props => {
return (
<Transition
transition={transition}
transaction={transaction}
currentUser={currentUser}
customer={currentCustomer}
provider={currentProvider}
listingTitle={currentListing.attributes.title}
intl={intl}
/>
);

View file

@ -217,6 +217,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -424,6 +425,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -633,6 +635,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -849,6 +852,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -1056,6 +1060,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -1265,6 +1270,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -1493,6 +1499,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -1700,6 +1707,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -1909,6 +1917,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -2125,6 +2134,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -2332,6 +2342,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -2541,6 +2552,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -2757,6 +2769,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -2964,6 +2977,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -3173,6 +3187,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -3412,6 +3427,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -3619,6 +3635,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -3828,6 +3845,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}

View file

@ -0,0 +1,11 @@
import ReviewRating from './ReviewRating';
export const ReviewRatingThree = {
component: ReviewRating,
props: { rating: 3 },
};
export const ReviewRatingFive = {
component: ReviewRating,
props: { rating: 5 },
};

View file

@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { IconReviewStar } from '../../components';
import * as propTypes from '../../util/propTypes';
const ReviewRating = props => {
const { className, rootClassName, reviewStarClassName, rating } = props;
const classes = classNames(rootClassName, className);
const stars = propTypes.REVIEW_RATINGS;
return (
<div className={classes}>
{stars.map(star => (
<IconReviewStar
key={`star-${star}`}
className={reviewStarClassName}
isFilled={star <= rating}
/>
))}
</div>
);
};
ReviewRating.defaultProps = {
rootClassName: null,
className: null,
reviewStarClassName: null,
};
const { string, oneOf } = PropTypes;
ReviewRating.propTypes = {
rating: oneOf(propTypes.REVIEW_RATINGS).isRequired,
reviewStartClassName: string,
rootClassName: string,
className: string,
};
export default ReviewRating;

View file

@ -211,6 +211,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -406,6 +407,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -571,6 +573,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -795,6 +798,7 @@ exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -990,6 +994,7 @@ exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -1155,6 +1160,7 @@ exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -1379,6 +1385,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -1574,6 +1581,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -1739,6 +1747,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -1963,6 +1972,7 @@ exports[`SaleDetailsPanel declined matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -2158,6 +2168,7 @@ exports[`SaleDetailsPanel declined matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -2323,6 +2334,7 @@ exports[`SaleDetailsPanel declined matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -2547,6 +2559,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -2742,6 +2755,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -2907,6 +2921,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -3141,6 +3156,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -3336,6 +3352,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}
@ -3529,6 +3546,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
"type": "listing",
},
"provider": null,
"reviews": Array [],
"type": "transaction",
}
}

View file

@ -80,6 +80,7 @@ export { default as PaginationLinks } from './PaginationLinks/PaginationLinks';
export { default as PrivacyPolicy } from './PrivacyPolicy/PrivacyPolicy';
export { default as Promised } from './Promised/Promised';
export { default as ResponsiveImage } from './ResponsiveImage/ResponsiveImage';
export { default as ReviewRating } from './ReviewRating/ReviewRating';
export { default as SaleDetailsPanel } from './SaleDetailsPanel/SaleDetailsPanel';
export { default as SearchMap } from './SearchMap/SearchMap';
export { default as SearchMapGroupLabel } from './SearchMapGroupLabel/SearchMapGroupLabel';

View file

@ -183,6 +183,7 @@ exports[`InboxPage matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -297,6 +298,7 @@ exports[`InboxPage matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -569,6 +571,7 @@ exports[`InboxPage matches snapshot 3`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}
@ -683,6 +686,7 @@ exports[`InboxPage matches snapshot 3`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}

View file

@ -117,7 +117,21 @@ export const fetchOrder = id => (dispatch, getState, sdk) => {
let txResponse = null;
return sdk.transactions
.show({ id, include: ['customer', 'provider', 'listing', 'booking'] }, { expand: true })
.show(
{
id,
include: [
'customer',
'provider',
'listing',
'booking',
'reviews',
'reviews.author',
'reviews.subject',
],
},
{ expand: true }
)
.then(response => {
txResponse = response;
const listingId = listingRelationship(response).id;

View file

@ -165,6 +165,7 @@ exports[`OrderPage matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}

View file

@ -159,6 +159,9 @@ export const fetchSale = id => (dispatch, getState, sdk) => {
'provider.profileImage',
'listing',
'booking',
'reviews',
'reviews.author',
'reviews.subject',
],
},
{ expand: true }

View file

@ -170,6 +170,7 @@ exports[`SalePage matches snapshot 1`] = `
},
"type": "user",
},
"reviews": Array [],
"type": "transaction",
}
}

View file

@ -36,6 +36,7 @@ import * as ModalInMobile from './components/ModalInMobile/ModalInMobile.example
import * as NamedLink from './components/NamedLink/NamedLink.example';
import * as PaginationLinks from './components/PaginationLinks/PaginationLinks.example';
import * as ResponsiveImage from './components/ResponsiveImage/ResponsiveImage.example';
import * as ReviewRating from './components/ReviewRating/ReviewRating.example';
import * as SelectField from './components/SelectField/SelectField.example';
import * as StripeBankAccountTokenInputField from './components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example';
import * as TabNav from './components/TabNav/TabNav.example';
@ -110,6 +111,7 @@ export {
PasswordResetForm,
PayoutDetailsForm,
ResponsiveImage,
ReviewRating,
SelectField,
SendMessageForm,
SignupForm,

View file

@ -1,16 +1,20 @@
{
"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.",
"ActivityFeed.ownTransitionAutoDecline": "Booking request expired. You did not reply on time.",
"ActivityFeed.ownTransitionDecline": "You declined the booking request.",
"ActivityFeed.ownTransitionRequest": "You requested to book { listingTitle }.",
"ActivityFeed.ownTransitionReview": "You left a review for { displayName }.",
"ActivityFeed.showOlderMessages": "Show older",
"ActivityFeed.today": "Today",
"ActivityFeed.transitionAccept": "{ displayName } accepted the booking request.",
"ActivityFeed.transitionAutoDecline": "Booking request expired. { displayName } did not reply on time.",
"ActivityFeed.transitionCancel": "The booking was cancelled.",
"ActivityFeed.transitionComplete": "The booking was completed.",
"ActivityFeed.transitionComplete": "The booking was completed. { reviewLink }",
"ActivityFeed.transitionDecline": "{ displayName } declined the booking request.",
"ActivityFeed.transitionRequest": "{ displayName } requested to book { listingTitle }.",
"ActivityFeed.transitionReview": "{ displayName } left a review for you. { reviewLink }",
"AuthenticationPage.emailAlreadyInUse": "An account with this email address already exists. Try logging in instead.",
"AuthenticationPage.fixEmail": "Whoops, typo in your email? {fixEmailLink}.",
"AuthenticationPage.fixEmailLinkText": "Fix it",

View file

@ -163,6 +163,16 @@ export const TX_TRANSITION_CANCEL = 'transition/cancel';
// with the mark-delivered transition.
export const TX_TRANSITION_MARK_DELIVERED = 'transition/mark-delivered';
// Review transitions
export const TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST = 'transition/review-by-provider-first';
export const TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND = 'transition/review-by-provider-second';
export const TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST = 'transition/review-by-customer-first';
export const TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND = 'transition/review-by-customer-second';
export const TX_TRANSITION_MARK_REVIEWED_BY_CUSTOMER = 'transition/mark-reviewed-by-customer';
export const TX_TRANSITION_MARK_REVIEWED_BY_PROVIDER = 'transition/mark-reviewed-by-provider';
export const TX_TRANSITION_AUTO_COMPLETE_WITHOUT_REVIEWS =
'transition/auto-complete-without-reviews';
export const TX_TRANSITIONS = [
TX_TRANSITION_PREAUTHORIZE,
TX_TRANSITION_ACCEPT,
@ -170,6 +180,13 @@ export const TX_TRANSITIONS = [
TX_TRANSITION_AUTO_DECLINE,
TX_TRANSITION_CANCEL,
TX_TRANSITION_MARK_DELIVERED,
TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST,
TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND,
TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST,
TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND,
TX_TRANSITION_MARK_REVIEWED_BY_CUSTOMER,
TX_TRANSITION_MARK_REVIEWED_BY_PROVIDER,
TX_TRANSITION_AUTO_COMPLETE_WITHOUT_REVIEWS,
];
// Roles of actors that perform transaction transitions
@ -207,6 +224,46 @@ export const txTransition = shape({
transition: oneOf(TX_TRANSITIONS).isRequired,
});
// Check if tx transition is followed by a state where
// reviews are completed
export const areReviewsCompleted = transition => {
return [
TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND,
TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND,
TX_TRANSITION_MARK_REVIEWED_BY_CUSTOMER,
TX_TRANSITION_MARK_REVIEWED_BY_PROVIDER,
TX_TRANSITION_AUTO_COMPLETE_WITHOUT_REVIEWS,
].includes(transition);
};
// Check if a user giving a review is related to
// given tx transition.
export const isReviewTransition = transition => {
return [
TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST,
TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST,
TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND,
TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND,
].includes(transition);
};
// Possible amount of stars in a review
export const REVIEW_RATINGS = [1, 2, 3, 4, 5];
// A review on a user
export const review = shape({
id: uuid.isRequired,
attributes: shape({
at: instanceOf(Date).isRequired,
content: string,
rating: oneOf(REVIEW_RATINGS),
state: string.isRequired,
type: string.isRequired,
}).isRequired,
author: user,
subject: user,
});
// Denormalised transaction object
export const transaction = shape({
id: uuid.isRequired,
@ -232,6 +289,7 @@ export const transaction = shape({
listing,
customer: user,
provider: user,
reviews: arrayOf(review),
});
// Denormalised transaction message

View file

@ -105,6 +105,7 @@ export const createTransaction = options => {
listing = null,
customer = null,
provider = null,
reviews = [],
lastTransitionedAt = new Date(Date.UTC(2017, 5, 1)),
transitions = [
createTxTransition({
@ -150,6 +151,7 @@ export const createTransaction = options => {
listing,
customer,
provider,
reviews,
};
};
@ -166,6 +168,22 @@ export const createMessage = (id, attributes = {}, includes = {}) => {
};
};
export const createReview = (id, attributes = {}, includes = {}) => {
return {
id: new UUID(id),
attributes: {
at: new Date(),
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
rating: 3,
state: 'public',
type: 'ofProvider',
...attributes,
},
...includes,
};
};
// Default config for currency formatting in tests and examples.
export const currencyConfig = {
style: 'currency',