mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 12:43:11 +10:00
Merge pull request #401 from sharetribe/handle-banned-users
Handle banned users
This commit is contained in:
commit
85fa852869
23 changed files with 468 additions and 74 deletions
|
|
@ -80,3 +80,8 @@
|
|||
font-weight: var(--fontWeightSemiBold);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.bannedUserIcon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
|||
171
src/components/Avatar/Avatar.example.js
Normal file
171
src/components/Avatar/Avatar.example.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import Avatar, { AvatarMedium, AvatarLarge } from './Avatar';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
|
||||
const { UUID } = types;
|
||||
|
||||
const bannedUser = {
|
||||
id: new UUID('banned-user'),
|
||||
type: 'user',
|
||||
attributes: {
|
||||
banned: true,
|
||||
},
|
||||
};
|
||||
|
||||
const userWithoutProfileImage = {
|
||||
id: new UUID('user-without-profile-image'),
|
||||
type: 'user',
|
||||
attributes: {
|
||||
profile: {
|
||||
displayName: 'No Profile',
|
||||
abbreviatedName: 'NP',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const userWithProfileImage = {
|
||||
id: new UUID('user-with-profile-image'),
|
||||
type: 'user',
|
||||
attributes: {
|
||||
profile: {
|
||||
displayName: 'Has Profile',
|
||||
abbreviatedName: 'HP',
|
||||
},
|
||||
},
|
||||
profileImage: {
|
||||
id: new UUID('profile-image'),
|
||||
type: 'image',
|
||||
attributes: {
|
||||
sizes: [
|
||||
{
|
||||
name: 'square-xlarge2x',
|
||||
width: 240,
|
||||
height: 240,
|
||||
url: 'https://lorempixel.com/240/240/people/',
|
||||
},
|
||||
{
|
||||
name: 'square-xlarge4x',
|
||||
width: 480,
|
||||
height: 480,
|
||||
url: 'https://lorempixel.com/480/480/people/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ================ Empty user ================ //
|
||||
|
||||
export const EmptyUser = {
|
||||
component: Avatar,
|
||||
props: {
|
||||
user: null,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
export const EmptyUserMedium = {
|
||||
component: AvatarMedium,
|
||||
props: {
|
||||
user: null,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
export const EmptyUserLarge = {
|
||||
component: AvatarLarge,
|
||||
props: {
|
||||
user: null,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
// ================ Banned user ================ //
|
||||
|
||||
export const BannedUser = {
|
||||
component: Avatar,
|
||||
props: {
|
||||
user: bannedUser,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
export const BannedUserMedium = {
|
||||
component: AvatarMedium,
|
||||
props: {
|
||||
user: bannedUser,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
export const BannedUserLarge = {
|
||||
component: AvatarLarge,
|
||||
props: {
|
||||
user: bannedUser,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
// ================ No profile image ================ //
|
||||
|
||||
export const WithoutProfileImageUser = {
|
||||
component: Avatar,
|
||||
props: {
|
||||
user: userWithoutProfileImage,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
export const WithoutProfileImageUserMedium = {
|
||||
component: AvatarMedium,
|
||||
props: {
|
||||
user: userWithoutProfileImage,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
export const WithoutProfileImageUserLarge = {
|
||||
component: AvatarLarge,
|
||||
props: {
|
||||
user: userWithoutProfileImage,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
// ================ Full user with profile image ================ //
|
||||
|
||||
export const WithProfileImageUser = {
|
||||
component: Avatar,
|
||||
props: {
|
||||
user: userWithProfileImage,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
export const WithProfileImageUserMedium = {
|
||||
component: AvatarMedium,
|
||||
props: {
|
||||
user: userWithProfileImage,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
||||
export const WithProfileImageUserLarge = {
|
||||
component: AvatarLarge,
|
||||
props: {
|
||||
user: userWithProfileImage,
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'avatar',
|
||||
};
|
||||
|
|
@ -1,16 +1,25 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { injectIntl, intlShape } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { ensureUser } from '../../util/data';
|
||||
import { ResponsiveImage } from '../../components/';
|
||||
import { ensureUser, userDisplayName, userAbbreviatedName } from '../../util/data';
|
||||
import { ResponsiveImage, IconBannedUser } from '../../components/';
|
||||
|
||||
import css from './Avatar.css';
|
||||
|
||||
const Avatar = props => {
|
||||
const { rootClassName, className, user } = props;
|
||||
export const AvatarComponent = props => {
|
||||
const { rootClassName, className, user, intl } = props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const avatarUser = ensureUser(user);
|
||||
const { displayName, abbreviatedName } = avatarUser.attributes.profile;
|
||||
const isBannedUser = avatarUser.attributes.banned;
|
||||
|
||||
const bannedUserDisplayName = intl.formatMessage({
|
||||
id: 'Avatar.bannedUserDisplayName',
|
||||
});
|
||||
const bannedUserAbbreviatedName = '';
|
||||
|
||||
const displayName = userDisplayName(avatarUser, bannedUserDisplayName);
|
||||
const abbreviatedName = userAbbreviatedName(avatarUser, bannedUserAbbreviatedName);
|
||||
|
||||
if (avatarUser.profileImage && avatarUser.profileImage.id) {
|
||||
return (
|
||||
|
|
@ -26,6 +35,12 @@ const Avatar = props => {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (isBannedUser) {
|
||||
return (
|
||||
<div className={classes} title={displayName}>
|
||||
<IconBannedUser className={css.bannedUserIcon} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// Placeholder avatar (initials)
|
||||
return (
|
||||
|
|
@ -38,18 +53,23 @@ const Avatar = props => {
|
|||
|
||||
const { string, oneOfType } = PropTypes;
|
||||
|
||||
Avatar.defaultProps = {
|
||||
AvatarComponent.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
user: null,
|
||||
};
|
||||
|
||||
Avatar.propTypes = {
|
||||
AvatarComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
user: oneOfType([propTypes.user, propTypes.currentUser]),
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const Avatar = injectIntl(AvatarComponent);
|
||||
|
||||
export default Avatar;
|
||||
|
||||
export const AvatarMedium = props => <Avatar rootClassName={css.mediumAvatar} {...props} />;
|
||||
|
|
|
|||
13
src/components/IconBannedUser/IconBannedUser.css
Normal file
13
src/components/IconBannedUser/IconBannedUser.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.backgroundFill {
|
||||
fill: var(--failColor);
|
||||
}
|
||||
|
||||
.foregroundFill {
|
||||
fill: var(--matterColorLight);
|
||||
}
|
||||
|
||||
.foregroundStroke {
|
||||
stroke: var(--matterColorLight);
|
||||
}
|
||||
30
src/components/IconBannedUser/IconBannedUser.js
Normal file
30
src/components/IconBannedUser/IconBannedUser.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
import css from './IconBannedUser.css';
|
||||
|
||||
const IconBannedUser = props => {
|
||||
const { className } = props;
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<circle className={css.backgroundFill} cx="20" cy="20" r="20" />
|
||||
<circle className={css.foregroundStroke} strokeWidth="3" cx="20" cy="20" r="13" />
|
||||
<path className={css.foregroundFill} d="M28.34 9.04l2.12 2.12-19.8 19.8-2.12-2.12z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
IconBannedUser.defaultProps = { className: null };
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
IconBannedUser.propTypes = { className: string };
|
||||
|
||||
export default IconBannedUser;
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
.heading {
|
||||
margin: 28px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||
import { injectIntl, intlShape, FormattedDate, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
import { ensureListing, ensureTransaction, ensureUser } from '../../util/data';
|
||||
import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
|
||||
import { BookingBreakdown, NamedLink, ResponsiveImage, AvatarMedium } from '../../components';
|
||||
|
||||
import css from './OrderDetailsPanel.css';
|
||||
|
|
@ -110,27 +110,43 @@ const orderMessage = (
|
|||
}
|
||||
};
|
||||
|
||||
const OrderDetailsPanel = props => {
|
||||
export const OrderDetailsPanelComponent = props => {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
transaction,
|
||||
intl,
|
||||
} = props;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const currentProvider = ensureUser(currentTransaction.provider);
|
||||
const currentCustomer = ensureUser(currentTransaction.customer);
|
||||
|
||||
const providerProfile = currentProvider.attributes.profile;
|
||||
const authorDisplayName = providerProfile.displayName;
|
||||
const customerDisplayName = currentCustomer.attributes.profile.displayName;
|
||||
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);
|
||||
const transactionState = currentTransaction.attributes.state;
|
||||
const lastTransitionedAt = currentTransaction.attributes.lastTransitionedAt;
|
||||
const lastTransition = currentTransaction.attributes.lastTransitione;
|
||||
|
||||
let listingLink = null;
|
||||
|
||||
if (currentListing.id && currentListing.attributes.title) {
|
||||
if (listingLoaded && currentListing.attributes.title) {
|
||||
const title = currentListing.attributes.title;
|
||||
const params = { id: currentListing.id.uuid, slug: createSlug(title) };
|
||||
listingLink = (
|
||||
|
|
@ -138,19 +154,30 @@ const OrderDetailsPanel = props => {
|
|||
{title}
|
||||
</NamedLink>
|
||||
);
|
||||
} else {
|
||||
listingLink = deletedListingOrderTitle;
|
||||
}
|
||||
|
||||
const listingTitle = currentListing.attributes.title;
|
||||
const listingTitle = currentListing.attributes.deleted
|
||||
? deletedListingTitle
|
||||
: currentListing.attributes.title;
|
||||
|
||||
const bookingInfo = breakdown(currentTransaction);
|
||||
const title = orderTitle(transactionState, listingLink, customerDisplayName, lastTransition);
|
||||
const message = orderMessage(
|
||||
const orderHeading = orderTitle(
|
||||
transactionState,
|
||||
listingLink,
|
||||
authorDisplayName,
|
||||
lastTransitionedAt,
|
||||
customerDisplayName,
|
||||
lastTransition
|
||||
);
|
||||
const message = listingDeleted
|
||||
? orderMessageDeletedListing
|
||||
: orderMessage(
|
||||
transactionState,
|
||||
listingLink,
|
||||
authorDisplayName,
|
||||
lastTransitionedAt,
|
||||
lastTransition
|
||||
);
|
||||
|
||||
const firstImage = currentListing.images && currentListing.images.length > 0
|
||||
? currentListing.images[0]
|
||||
|
|
@ -177,7 +204,7 @@ const OrderDetailsPanel = props => {
|
|||
<AvatarMedium user={currentProvider} />
|
||||
</div>
|
||||
<div className={css.orderInfo}>
|
||||
<h1 className={css.title}>{title}</h1>
|
||||
<h1 className={css.heading}>{orderHeading}</h1>
|
||||
<div className={css.message}>
|
||||
{message}
|
||||
</div>
|
||||
|
|
@ -225,7 +252,7 @@ const OrderDetailsPanel = props => {
|
|||
);
|
||||
};
|
||||
|
||||
OrderDetailsPanel.defaultProps = {
|
||||
OrderDetailsPanelComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
lastTransition: null,
|
||||
|
|
@ -233,10 +260,15 @@ OrderDetailsPanel.defaultProps = {
|
|||
|
||||
const { string } = PropTypes;
|
||||
|
||||
OrderDetailsPanel.propTypes = {
|
||||
OrderDetailsPanelComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
transaction: propTypes.transaction.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
const OrderDetailsPanel = injectIntl(OrderDetailsPanelComponent);
|
||||
|
||||
export default OrderDetailsPanel;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import { shallow } from 'enzyme';
|
|||
import { types } from '../../util/sdkLoader';
|
||||
import { createBooking, createListing, createUser, createTransaction } from '../../util/test-data';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import { BookingBreakdown } from '../../components';
|
||||
import OrderDetailsPanel from './OrderDetailsPanel.js';
|
||||
import { OrderDetailsPanelComponent } from './OrderDetailsPanel.js';
|
||||
|
||||
describe('OrderDetailsPanel', () => {
|
||||
it('matches snapshot', () => {
|
||||
|
|
@ -21,7 +22,7 @@ describe('OrderDetailsPanel', () => {
|
|||
provider: createUser('provider'),
|
||||
customer: createUser('customer'),
|
||||
});
|
||||
const tree = renderShallow(<OrderDetailsPanel transaction={tx} />);
|
||||
const tree = renderShallow(<OrderDetailsPanelComponent transaction={tx} intl={fakeIntl} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders correct total price', () => {
|
||||
|
|
@ -38,7 +39,7 @@ describe('OrderDetailsPanel', () => {
|
|||
provider: createUser('provider'),
|
||||
customer: createUser('customer'),
|
||||
});
|
||||
const panel = shallow(<OrderDetailsPanel transaction={tx} />);
|
||||
const panel = shallow(<OrderDetailsPanelComponent transaction={tx} intl={fakeIntl} />);
|
||||
const breakdownProps = panel.find(BookingBreakdown).props();
|
||||
expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||
import { injectIntl, intlShape, FormattedDate, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
import { ensureListing, ensureTransaction, ensureUser } from '../../util/data';
|
||||
import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
|
||||
import {
|
||||
AvatarLarge,
|
||||
AvatarMedium,
|
||||
|
|
@ -99,7 +99,7 @@ const saleMessage = (saleState, customerName, lastTransitionedAt, lastTransition
|
|||
}
|
||||
};
|
||||
|
||||
const SaleDetailsPanel = props => {
|
||||
export const SaleDetailsPanelComponent = props => {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
|
|
@ -107,12 +107,19 @@ const SaleDetailsPanel = props => {
|
|||
onAcceptSale,
|
||||
onRejectSale,
|
||||
acceptOrRejectInProgress,
|
||||
intl,
|
||||
} = props;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const currentCustomer = ensureUser(currentTransaction.customer);
|
||||
const customerLoaded = !!currentCustomer.id;
|
||||
const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned;
|
||||
|
||||
const customerDisplayName = currentCustomer.attributes.profile.displayName;
|
||||
const bannedUserDisplayName = intl.formatMessage({
|
||||
id: 'SaleDetailsPanel.bannedUserDisplayName',
|
||||
});
|
||||
|
||||
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
|
||||
const transactionState = currentTransaction.attributes.state;
|
||||
const lastTransitionedAt = currentTransaction.attributes.lastTransitionedAt;
|
||||
const lastTransition = currentTransaction.attributes.lastTransition;
|
||||
|
|
@ -132,12 +139,11 @@ const SaleDetailsPanel = props => {
|
|||
const bookingInfo = breakdown(currentTransaction);
|
||||
|
||||
const title = saleTitle(transactionState, listingLink, customerDisplayName, lastTransition);
|
||||
const message = saleMessage(
|
||||
transactionState,
|
||||
customerDisplayName,
|
||||
lastTransitionedAt,
|
||||
lastTransition
|
||||
);
|
||||
const message = isCustomerBanned
|
||||
? intl.formatMessage({
|
||||
id: 'SaleDetailsPanel.customerBannedStatus',
|
||||
})
|
||||
: saleMessage(transactionState, customerDisplayName, lastTransitionedAt, lastTransition);
|
||||
|
||||
const listingTitle = currentListing.attributes.title;
|
||||
const firstImage = currentListing.images && currentListing.images.length > 0
|
||||
|
|
@ -146,7 +152,8 @@ const SaleDetailsPanel = props => {
|
|||
|
||||
const isPreauthorizedState = currentTransaction.attributes.state ===
|
||||
propTypes.TX_STATE_PREAUTHORIZED;
|
||||
const actionButtons = isPreauthorizedState
|
||||
const canShowButtons = isPreauthorizedState && !isCustomerBanned;
|
||||
const actionButtons = canShowButtons
|
||||
? <div className={css.actionButtons}>
|
||||
<SecondaryButton
|
||||
className={css.rejectButton}
|
||||
|
|
@ -224,7 +231,7 @@ const SaleDetailsPanel = props => {
|
|||
);
|
||||
};
|
||||
|
||||
SaleDetailsPanel.defaultProps = {
|
||||
SaleDetailsPanelComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
lastTransition: null,
|
||||
|
|
@ -232,13 +239,18 @@ SaleDetailsPanel.defaultProps = {
|
|||
|
||||
const { string, func, bool } = PropTypes;
|
||||
|
||||
SaleDetailsPanel.propTypes = {
|
||||
SaleDetailsPanelComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
transaction: propTypes.transaction.isRequired,
|
||||
onAcceptSale: func.isRequired,
|
||||
onRejectSale: func.isRequired,
|
||||
acceptOrRejectInProgress: bool.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const SaleDetailsPanel = injectIntl(SaleDetailsPanelComponent);
|
||||
|
||||
export default SaleDetailsPanel;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import { shallow } from 'enzyme';
|
|||
import { types } from '../../util/sdkLoader';
|
||||
import { createTransaction, createBooking, createListing, createUser } from '../../util/test-data';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import { BookingBreakdown } from '../../components';
|
||||
import SaleDetailsPanel from './SaleDetailsPanel.js';
|
||||
import { SaleDetailsPanelComponent } from './SaleDetailsPanel.js';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
|
|
@ -29,8 +30,9 @@ describe('SaleDetailsPanel', () => {
|
|||
onAcceptSale: noop,
|
||||
onRejectSale: noop,
|
||||
acceptOrRejectInProgress: false,
|
||||
intl: fakeIntl,
|
||||
};
|
||||
const tree = renderShallow(<SaleDetailsPanel {...props} />);
|
||||
const tree = renderShallow(<SaleDetailsPanelComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('renders correct total price', () => {
|
||||
|
|
@ -53,8 +55,9 @@ describe('SaleDetailsPanel', () => {
|
|||
onAcceptSale: noop,
|
||||
onRejectSale: noop,
|
||||
acceptOrRejectInProgress: false,
|
||||
intl: fakeIntl,
|
||||
};
|
||||
const panel = shallow(<SaleDetailsPanel {...props} />);
|
||||
const panel = shallow(<SaleDetailsPanelComponent {...props} />);
|
||||
const breakdownProps = panel.find(BookingBreakdown).props();
|
||||
|
||||
// Total price for the provider should be transaction total minus the commission.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import ExternalLink from './ExternalLink/ExternalLink';
|
|||
import ExpandingTextarea from './ExpandingTextarea/ExpandingTextarea';
|
||||
import FilterPanel from './FilterPanel/FilterPanel';
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import IconBannedUser from './IconBannedUser/IconBannedUser';
|
||||
import IconEmailAttention from './IconEmailAttention/IconEmailAttention';
|
||||
import IconEmailSent from './IconEmailSent/IconEmailSent';
|
||||
import IconEmailSuccess from './IconEmailSuccess/IconEmailSuccess';
|
||||
|
|
@ -90,6 +91,7 @@ export {
|
|||
ExternalLink,
|
||||
FilterPanel,
|
||||
HeroSection,
|
||||
IconBannedUser,
|
||||
IconEmailAttention,
|
||||
IconEmailSent,
|
||||
IconEmailSuccess,
|
||||
|
|
|
|||
|
|
@ -175,10 +175,6 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover .itemUsername {
|
||||
color: var(--marketplaceColor);
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-bottom: 21px;
|
||||
padding-bottom: 18px;
|
||||
|
|
@ -194,6 +190,11 @@
|
|||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover .itemUsername {
|
||||
/* Hightlight the username when the whole item is hovered */
|
||||
color: var(--marketplaceColor);
|
||||
}
|
||||
}
|
||||
|
||||
.itemAvatar {
|
||||
|
|
@ -327,6 +328,10 @@
|
|||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.stateName {
|
||||
/* This class is empty on purpose, it is used below for banned users */
|
||||
}
|
||||
|
||||
.stateRequested,
|
||||
.statePending {
|
||||
font-weight: var(--fontWeightSemiBold);
|
||||
|
|
@ -376,3 +381,13 @@
|
|||
.lastTransitionedAtDelivered {
|
||||
color: var(--matterColorAnti);
|
||||
}
|
||||
|
||||
.bannedUserLink {
|
||||
& .itemUsername,
|
||||
&:hover .itemUsername,
|
||||
& .bookingInfo,
|
||||
& .stateName,
|
||||
& .lastTransitionedAt {
|
||||
color: var(--matterColorAnti);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '../../components';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { formatMoney } from '../../util/currency';
|
||||
import { userDisplayName } from '../../util/data';
|
||||
import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
|
||||
import { sendVerificationEmail } from '../../ducks/user.duck';
|
||||
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
|
||||
|
|
@ -93,7 +94,11 @@ export const InboxItem = props => {
|
|||
const isOrder = type === 'order';
|
||||
|
||||
const otherUser = isOrder ? provider : customer;
|
||||
const otherUserDisplayName = otherUser.attributes.profile.displayName;
|
||||
const bannedUserDisplayName = intl.formatMessage({
|
||||
id: 'InboxPage.bannedUserDisplayName',
|
||||
});
|
||||
const isOtherUserBanned = otherUser.attributes.banned;
|
||||
const otherUserDisplayName = userDisplayName(otherUser, bannedUserDisplayName);
|
||||
|
||||
const stateData = txState(intl, tx, isOrder);
|
||||
const isSaleNotification = !isOrder && tx.attributes.state === propTypes.TX_STATE_PREAUTHORIZED;
|
||||
|
|
@ -104,9 +109,13 @@ export const InboxItem = props => {
|
|||
const bookingPrice = isOrder ? tx.attributes.payinTotal : tx.attributes.payoutTotal;
|
||||
const price = formatMoney(intl, bookingPrice);
|
||||
|
||||
const linkClasses = classNames(css.itemLink, {
|
||||
[css.bannedUserLink]: isOtherUserBanned,
|
||||
});
|
||||
|
||||
return (
|
||||
<NamedLink
|
||||
className={css.itemLink}
|
||||
className={linkClasses}
|
||||
name={isOrder ? 'OrderDetailsPage' : 'SaleDetailsPage'}
|
||||
params={{ id: tx.id.uuid }}
|
||||
>
|
||||
|
|
@ -126,7 +135,7 @@ export const InboxItem = props => {
|
|||
</div>
|
||||
</div>
|
||||
<div className={css.itemState}>
|
||||
<div className={stateData.stateClassName}>{stateData.state}</div>
|
||||
<div className={classNames(css.stateName, stateData.stateClassName)}>{stateData.state}</div>
|
||||
<div
|
||||
className={classNames(css.lastTransitionedAt, stateData.lastTransitionedAtClassName)}
|
||||
title={lastTransitionedAt.long}
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ exports[`InboxPage matches snapshot 2`] = `
|
|||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
className="">
|
||||
InboxPage.stateRequested
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -605,7 +605,7 @@ exports[`InboxPage matches snapshot 4`] = `
|
|||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
className="">
|
||||
InboxPage.statePending
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ describe('ListingPage', () => {
|
|||
|
||||
return showListing(id)(dispatch, null, sdk).then(data => {
|
||||
expect(data).toEqual(response);
|
||||
expect(show.mock.calls).toEqual([[{ id, include: ['author', 'author.profileImage', 'images'] }]]);
|
||||
expect(show.mock.calls).toEqual([
|
||||
[{ id, include: ['author', 'author.profileImage', 'images'] }],
|
||||
]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
[showListingRequest(id)],
|
||||
[expect.anything()], // fetchCurrentUser() call
|
||||
|
|
@ -76,7 +78,9 @@ describe('ListingPage', () => {
|
|||
},
|
||||
e => {
|
||||
expect(e).toEqual(error);
|
||||
expect(show.mock.calls).toEqual([[{ id, include: ['author', 'author.profileImage', 'images'] }]]);
|
||||
expect(show.mock.calls).toEqual([
|
||||
[{ id, include: ['author', 'author.profileImage', 'images'] }],
|
||||
]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
[showListingRequest(id)],
|
||||
[expect.anything()], // fetchCurrentUser() call
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { types } from '../../util/sdkLoader';
|
||||
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
|
||||
import { updatedEntities, denormalisedEntities } from '../../util/data';
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
|
|
@ -56,11 +57,20 @@ export const fetchOrder = id =>
|
|||
.then(response => {
|
||||
txResponse = response;
|
||||
const listingId = listingRelationship(response).id;
|
||||
const entities = updatedEntities({}, response.data);
|
||||
const denormalised = denormalisedEntities(entities, 'listing', [listingId]);
|
||||
const listing = denormalised[0];
|
||||
|
||||
return sdk.listings.show({
|
||||
id: listingId,
|
||||
include: ['author', 'author.profileImage', 'images'],
|
||||
});
|
||||
const canFetchListing = listing && listing.attributes && !listing.attributes.deleted;
|
||||
|
||||
if (canFetchListing) {
|
||||
return sdk.listings.show({
|
||||
id: listingId,
|
||||
include: ['author', 'author.profileImage', 'images'],
|
||||
});
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
dispatch(addMarketplaceEntities(txResponse));
|
||||
|
|
|
|||
|
|
@ -43,10 +43,8 @@ exports[`OrderPage matches snapshot 1`] = `
|
|||
onResendVerificationEmail={[Function]}
|
||||
sendVerificationEmailError={null}
|
||||
sendVerificationEmailInProgress={false} />
|
||||
<OrderDetailsPanel
|
||||
<InjectIntl(Component)
|
||||
className="undefined"
|
||||
lastTransition={null}
|
||||
rootClassName={null}
|
||||
transaction={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import * as propTypes from '../../util/propTypes';
|
|||
import { sendVerificationEmail } from '../../ducks/user.duck';
|
||||
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
|
||||
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
|
||||
import { recoverPassword, retypePasswordRecoveryEmail, clearPasswordRecoveryError } from './PasswordRecoveryPage.duck';
|
||||
import {
|
||||
recoverPassword,
|
||||
retypePasswordRecoveryEmail,
|
||||
clearPasswordRecoveryError,
|
||||
} from './PasswordRecoveryPage.duck';
|
||||
import { PageLayout, Topbar, InlineTextButton, KeysIcon } from '../../components';
|
||||
import { PasswordRecoveryForm } from '../../containers';
|
||||
|
||||
|
|
|
|||
|
|
@ -44,13 +44,11 @@ exports[`SalePage matches snapshot 1`] = `
|
|||
sendVerificationEmailError={null}
|
||||
sendVerificationEmailInProgress={false} />
|
||||
<div>
|
||||
<SaleDetailsPanel
|
||||
<InjectIntl(Component)
|
||||
acceptOrRejectInProgress={false}
|
||||
className="undefined"
|
||||
lastTransition={null}
|
||||
onAcceptSale={[Function]}
|
||||
onRejectSale={[Function]}
|
||||
rootClassName={null}
|
||||
transaction={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// components
|
||||
import * as AddImages from './components/AddImages/AddImages.example';
|
||||
import * as Avatar from './components/Avatar/Avatar.example';
|
||||
import * as BirthdayInputField from './components/BirthdayInputField/BirthdayInputField.example';
|
||||
import * as BookingBreakdown from './components/BookingBreakdown/BookingBreakdown.example';
|
||||
import * as Button from './components/Button/Button.example';
|
||||
|
|
@ -52,6 +53,7 @@ import * as Typography from './containers/StyleguidePage/Typography.example';
|
|||
|
||||
export {
|
||||
AddImages,
|
||||
Avatar,
|
||||
BirthdayInputField,
|
||||
BookingBreakdown,
|
||||
BookingDatesForm,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"AuthenticationPage.verifyEmailClose": "LATER",
|
||||
"AuthenticationPage.verifyEmailText": "Thanks for signing up! There's one quick step left. To be able to contact you, we need you to verify your email address. Please click the link we sent to {email}.",
|
||||
"AuthenticationPage.verifyEmailTitle": "{name}, check your inbox to verify your email",
|
||||
"Avatar.bannedUserDisplayName": "Banned user",
|
||||
"BookingBreakdown.bookingPeriod": "{bookingStart} – {bookingEnd}",
|
||||
"BookingBreakdown.commission": "Saunatime fee",
|
||||
"BookingBreakdown.nightCount": "{count, number} {count, plural, one {night} other {nights}}",
|
||||
|
|
@ -130,6 +131,7 @@
|
|||
"HeroSection.title": "Book saunas everywhere.",
|
||||
"ImageCarousel.imageAltText": "Image {index}/{count}",
|
||||
"ImageFromFile.couldNotReadFile": "Could not read file",
|
||||
"InboxPage.bannedUserDisplayName": "Banned user",
|
||||
"InboxPage.fetchFailed": "Could not load all messages. Please try again.",
|
||||
"InboxPage.noOrdersFound": "You haven't made any bookings.",
|
||||
"InboxPage.noSalesFound": "Nobody has booked anything from you yet.",
|
||||
|
|
@ -191,8 +193,12 @@
|
|||
"MapPriceMarker.unsupportedPrice": "({currency})",
|
||||
"Modal.close": "CLOSE",
|
||||
"Modal.closeModal": "Close modal",
|
||||
"OrderDetailsPanel.bannedUserDisplayName": "Banned user",
|
||||
"OrderDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
|
||||
"OrderDetailsPanel.deletedListingOrderTitle": "a listing",
|
||||
"OrderDetailsPanel.deletedListingTitle": "Deleted listing",
|
||||
"OrderDetailsPanel.hostedBy": "Hosted by {name}",
|
||||
"OrderDetailsPanel.messageDeletedListing": "However, the listing is deleted and cannot be viewed anymore.",
|
||||
"OrderDetailsPanel.orderAcceptedStatus": "{providerName} accepted the request on {transitionDate}.",
|
||||
"OrderDetailsPanel.orderAcceptedSubtitle": "You have booked {listingLink}",
|
||||
"OrderDetailsPanel.orderAcceptedTitle": "Woohoo {customerName}!",
|
||||
|
|
@ -312,7 +318,9 @@
|
|||
"ProfileSettingsForm.yourProfilePicture": "Your profile picture",
|
||||
"ProfileSettingsPage.title": "Profile settings",
|
||||
"ResponsiveImage.noImage": "No image",
|
||||
"SaleDetailsPanel.bannedUserDisplayName": "Banned user",
|
||||
"SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
|
||||
"SaleDetailsPanel.customerBannedStatus": "The user made the request, but was later banned. You cannot accept or reject the request.",
|
||||
"SaleDetailsPanel.listingAcceptedTitle": "Woohoo! You accepted a request from {customerName} for {listingLink}",
|
||||
"SaleDetailsPanel.listingDeliveredTitle": "{customerName} booked {listingLink}",
|
||||
"SaleDetailsPanel.listingRejectedTitle": "{customerName} requested to book {listingLink}",
|
||||
|
|
|
|||
|
|
@ -163,3 +163,54 @@ export const ensureCurrentUser = user => {
|
|||
const empty = { id: null, type: 'current-user', attributes: { profile: {} }, profileImage: {} };
|
||||
return { ...empty, ...user };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the display name of the given user. This function handles
|
||||
* missing data (e.g. when the user object is still being downloaded),
|
||||
* fully loaded users, as well as banned users.
|
||||
*
|
||||
* For banned users, a translated name should be provided.
|
||||
*
|
||||
* @param {propTypes.user} user
|
||||
* @param {String} bannedUserDisplayName
|
||||
*
|
||||
* @return {String} display name that can be rendered in the UI
|
||||
*/
|
||||
export const userDisplayName = (user, bannedUserDisplayName) => {
|
||||
const hasAttributes = user && user.attributes;
|
||||
const hasProfile = hasAttributes && user.attributes.profile;
|
||||
|
||||
if (hasAttributes && user.attributes.banned) {
|
||||
return bannedUserDisplayName;
|
||||
} else if (hasProfile) {
|
||||
return user.attributes.profile.displayName;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the abbreviated name of the given user. This function handles
|
||||
* missing data (e.g. when the user object is still being downloaded),
|
||||
* fully loaded users, as well as banned users.
|
||||
*
|
||||
* For banned users, a translated name should be provided.
|
||||
*
|
||||
* @param {propTypes.user} user
|
||||
* @param {String} bannedUserAbbreviatedName
|
||||
*
|
||||
* @return {String} abbreviated name that can be rendered in the UI
|
||||
* (e.g. in Avatar initials)
|
||||
*/
|
||||
export const userAbbreviatedName = (user, bannedUserAbbreviatedName) => {
|
||||
const hasAttributes = user && user.attributes;
|
||||
const hasProfile = hasAttributes && user.attributes.profile;
|
||||
|
||||
if (hasAttributes && user.attributes.banned) {
|
||||
return bannedUserAbbreviatedName;
|
||||
} else if (hasProfile) {
|
||||
return user.attributes.profile.abbreviatedName;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import Decimal from 'decimal.js';
|
|||
import { types as sdkTypes } from './sdkLoader';
|
||||
|
||||
const { UUID, LatLng, LatLngBounds, Money } = sdkTypes;
|
||||
const { arrayOf, bool, func, instanceOf, number, oneOf, shape, string } = PropTypes;
|
||||
const { arrayOf, bool, func, instanceOf, number, oneOf, oneOfType, shape, string } = PropTypes;
|
||||
|
||||
// Fixed value
|
||||
export const value = val => oneOf([val]);
|
||||
|
|
@ -85,7 +85,7 @@ export const user = shape({
|
|||
profile: shape({
|
||||
displayName: string.isRequired,
|
||||
abbreviatedName: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -105,18 +105,24 @@ export const image = shape({
|
|||
}),
|
||||
});
|
||||
|
||||
const listingAttributes = shape({
|
||||
title: string.isRequired,
|
||||
description: string.isRequired,
|
||||
address: string.isRequired,
|
||||
geolocation: latlng.isRequired,
|
||||
open: bool.isRequired,
|
||||
price: money,
|
||||
});
|
||||
|
||||
const deletedListingAttributes = shape({
|
||||
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,
|
||||
open: bool.isRequired,
|
||||
price: money,
|
||||
}),
|
||||
attributes: oneOfType([listingAttributes, deletedListingAttributes]).isRequired,
|
||||
author: user,
|
||||
images: arrayOf(image),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue