mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-27 19:42:11 +10:00
Merge pull request #551 from sharetribe/order-page-send-message
Send messages in Order page
This commit is contained in:
commit
69480bf823
20 changed files with 1091 additions and 179 deletions
|
|
@ -91,6 +91,7 @@
|
|||
width: 28px;
|
||||
height: 28px;
|
||||
stroke: var(--matterColorLight);
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@
|
|||
height: 28px;
|
||||
|
||||
stroke: var(--marketplaceColor);
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ const IconSpinner = props => {
|
|||
<svg
|
||||
className={classes}
|
||||
viewBox="0 0 30 30"
|
||||
strokeWidth="3"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const MessagesComponent = props => {
|
|||
return (
|
||||
<ul className={classes}>
|
||||
{messages.map(m => (
|
||||
<li key={m.id.uuid} className={css.messageItem}>
|
||||
<li id={`msg-${m.id.uuid}`} key={m.id.uuid} className={css.messageItem}>
|
||||
{msg(m)}
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ exports[`Messages matches snapshot 1`] = `
|
|||
>
|
||||
<li
|
||||
className={undefined}
|
||||
id="msg-msg1"
|
||||
>
|
||||
<div
|
||||
className={undefined}
|
||||
|
|
@ -41,6 +42,7 @@ exports[`Messages matches snapshot 1`] = `
|
|||
</li>
|
||||
<li
|
||||
className={undefined}
|
||||
id="msg-msg2"
|
||||
>
|
||||
<div
|
||||
className={undefined}
|
||||
|
|
|
|||
|
|
@ -83,22 +83,26 @@
|
|||
}
|
||||
|
||||
.orderInfo {
|
||||
margin-bottom: 210px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
max-width: 538px;
|
||||
margin-right: 108px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 27px 24px 0 24px;
|
||||
margin: 29px 24px 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 25px 24px 0 24px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
max-width: 100%;
|
||||
margin: 177px 0 0 0;
|
||||
margin: 175px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +156,9 @@
|
|||
width: 409px;
|
||||
display: block;
|
||||
padding-bottom: 55px;
|
||||
background-color: var(--matterColorLight);
|
||||
border: 1px solid var(--matterColorNegative);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,12 +173,9 @@
|
|||
margin: 1px 0 0 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 122px;
|
||||
margin-top: 121px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
background-color: var(--matterColorLight);
|
||||
border: 1px solid var(--matterColorNegative);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +193,7 @@
|
|||
margin-bottom: 10px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 13px;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -262,3 +266,24 @@
|
|||
margin-top: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
.sendMessageForm {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-shadow: var(--boxShadowBottomForm);
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 47px;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sendMessageFormFocusedInMobileSafari {
|
||||
position: absolute;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
|
||||
import { isMobileSafari } from '../../util/userAgent';
|
||||
import {
|
||||
BookingBreakdown,
|
||||
NamedLink,
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
AvatarMedium,
|
||||
Messages,
|
||||
} from '../../components';
|
||||
import { SendMessageForm } from '../../containers';
|
||||
|
||||
import css from './OrderDetailsPanel.css';
|
||||
|
||||
|
|
@ -77,129 +79,150 @@ const orderMessage = (transaction, providerName) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
export const OrderDetailsPanelComponent = props => {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
currentUser,
|
||||
transaction,
|
||||
messages,
|
||||
initialMessageFailed,
|
||||
fetchMessagesError,
|
||||
intl,
|
||||
} = props;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const currentProvider = ensureUser(currentTransaction.provider);
|
||||
const currentCustomer = ensureUser(currentTransaction.customer);
|
||||
|
||||
const listingLoaded = !!currentListing.id;
|
||||
const listingDeleted = listingLoaded && currentListing.attributes.deleted;
|
||||
|
||||
const bannedUserDisplayName = intl.formatMessage({
|
||||
id: 'OrderDetailsPanel.bannedUserDisplayName',
|
||||
});
|
||||
const deletedListingTitle = intl.formatMessage({
|
||||
id: 'OrderDetailsPanel.deletedListingTitle',
|
||||
});
|
||||
const deletedListingOrderTitle = intl.formatMessage({
|
||||
id: 'OrderDetailsPanel.deletedListingOrderTitle',
|
||||
});
|
||||
const orderMessageDeletedListing = intl.formatMessage({
|
||||
id: 'OrderDetailsPanel.messageDeletedListing',
|
||||
});
|
||||
|
||||
const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
|
||||
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
|
||||
|
||||
let listingLink = null;
|
||||
|
||||
if (listingLoaded && currentListing.attributes.title) {
|
||||
const title = currentListing.attributes.title;
|
||||
const params = { id: currentListing.id.uuid, slug: createSlug(title) };
|
||||
listingLink = (
|
||||
<NamedLink name="ListingPage" params={params}>
|
||||
{title}
|
||||
</NamedLink>
|
||||
);
|
||||
} else {
|
||||
listingLink = deletedListingOrderTitle;
|
||||
export class OrderDetailsPanelComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { sendMessageFormFocused: false };
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
currentUser,
|
||||
transaction,
|
||||
messages,
|
||||
initialMessageFailed,
|
||||
fetchMessagesError,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
onSendMessage,
|
||||
onResetForm,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
const listingTitle = currentListing.attributes.deleted
|
||||
? deletedListingTitle
|
||||
: currentListing.attributes.title;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const currentProvider = ensureUser(currentTransaction.provider);
|
||||
const currentCustomer = ensureUser(currentTransaction.customer);
|
||||
|
||||
const bookingInfo = breakdown(currentTransaction);
|
||||
const orderHeading = orderTitle(currentTransaction, listingLink, customerDisplayName);
|
||||
const orderInfoText = listingDeleted
|
||||
? orderMessageDeletedListing
|
||||
: orderMessage(currentTransaction, authorDisplayName);
|
||||
const showInfoMessage = !!orderInfoText;
|
||||
const listingLoaded = !!currentListing.id;
|
||||
const listingDeleted = listingLoaded && currentListing.attributes.deleted;
|
||||
|
||||
const firstImage =
|
||||
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
|
||||
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 messagesContainerClasses = classNames(css.messagesContainer, {
|
||||
[css.messagesContainerWithInfoAbove]: showInfoMessage,
|
||||
});
|
||||
const showMessages = messages.length > 0 || initialMessageFailed || fetchMessagesError;
|
||||
const messagesContainer = showMessages ? (
|
||||
<div className={messagesContainerClasses}>
|
||||
<h3 className={css.messagesHeading}>
|
||||
<FormattedMessage id="OrderDetailsPanel.messagesHeading" />
|
||||
</h3>
|
||||
{initialMessageFailed ? (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="OrderDetailsPanel.initialMessageFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
{fetchMessagesError ? (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="OrderDetailsPanel.messageLoadingFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
<Messages className={css.messages} messages={messages} currentUser={currentUser} />
|
||||
</div>
|
||||
) : null;
|
||||
const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
|
||||
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
let listingLink = null;
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.container}>
|
||||
<div className={css.orderInfo}>
|
||||
<div className={css.imageWrapperMobile}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
rootClassName={css.rootForImage}
|
||||
alt={listingTitle}
|
||||
image={firstImage}
|
||||
nameSet={[
|
||||
{ name: 'landscape-crop', size: '400w' },
|
||||
{ name: 'landscape-crop2x', size: '800w' },
|
||||
]}
|
||||
sizes="100vw"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(css.avatarWrapper, css.avatarMobile)}>
|
||||
<AvatarMedium user={currentProvider} />
|
||||
</div>
|
||||
<h1 className={css.heading}>{orderHeading}</h1>
|
||||
{showInfoMessage ? <p className={css.orderInfoText}>{orderInfoText}</p> : null}
|
||||
{showInfoMessage ? <hr className={css.infoTextDivider} /> : null}
|
||||
<div className={css.breakdownMobile}>
|
||||
<h3 className={css.bookingBreakdownTitle}>
|
||||
<FormattedMessage id="OrderDetailsPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
{bookingInfo}
|
||||
</div>
|
||||
{messagesContainer}
|
||||
</div>
|
||||
<div className={css.bookingBreakdownContainer}>
|
||||
<div className={css.breakdownDesktop}>
|
||||
<div className={css.breakdownImageWrapper}>
|
||||
if (listingLoaded && currentListing.attributes.title) {
|
||||
const title = currentListing.attributes.title;
|
||||
const params = { id: currentListing.id.uuid, slug: createSlug(title) };
|
||||
listingLink = (
|
||||
<NamedLink name="ListingPage" params={params}>
|
||||
{title}
|
||||
</NamedLink>
|
||||
);
|
||||
} else {
|
||||
listingLink = deletedListingOrderTitle;
|
||||
}
|
||||
|
||||
const listingTitle = currentListing.attributes.deleted
|
||||
? deletedListingTitle
|
||||
: currentListing.attributes.title;
|
||||
|
||||
const bookingInfo = breakdown(currentTransaction);
|
||||
const orderHeading = orderTitle(currentTransaction, listingLink, customerDisplayName);
|
||||
const orderInfoText = listingDeleted
|
||||
? orderMessageDeletedListing
|
||||
: orderMessage(currentTransaction, authorDisplayName);
|
||||
const showInfoMessage = !!orderInfoText;
|
||||
|
||||
const firstImage =
|
||||
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
|
||||
|
||||
const messagesContainerClasses = classNames(css.messagesContainer, {
|
||||
[css.messagesContainerWithInfoAbove]: showInfoMessage,
|
||||
});
|
||||
const showMessages = messages.length > 0 || initialMessageFailed || fetchMessagesError;
|
||||
const messagesContainer = showMessages ? (
|
||||
<div className={messagesContainerClasses}>
|
||||
<h3 className={css.messagesHeading}>
|
||||
<FormattedMessage id="OrderDetailsPanel.messagesHeading" />
|
||||
</h3>
|
||||
{initialMessageFailed ? (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="OrderDetailsPanel.initialMessageFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
{fetchMessagesError ? (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="OrderDetailsPanel.messageLoadingFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
<Messages className={css.messages} messages={messages} currentUser={currentUser} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const sendMessagePlaceholder = intl.formatMessage(
|
||||
{ id: 'OrderDetailsPanel.sendMessagePlaceholder' },
|
||||
{ name: 'Juho' }
|
||||
);
|
||||
|
||||
const sendMessageFormName = 'OrderDetailsPanel.SendMessageForm';
|
||||
const scrollToMessage = messageId => {
|
||||
const selector = `#msg-${messageId.uuid}`;
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
el.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
const isMobSaf = isMobileSafari();
|
||||
const handleSendMessageFormFocus = () => {
|
||||
this.setState({ sendMessageFormFocused: true });
|
||||
if (isMobSaf) {
|
||||
// Scroll to bottom
|
||||
window.scroll({ top: document.body.scrollHeight, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
const handleSendMessageFormBlur = () => {
|
||||
this.setState({ sendMessageFormFocused: false });
|
||||
};
|
||||
const handleMessageSubmit = values => {
|
||||
return onSendMessage(currentTransaction.id, values.message)
|
||||
.then(messageId => {
|
||||
onResetForm(sendMessageFormName);
|
||||
scrollToMessage(messageId);
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
const sendMessageFormClasses = classNames(css.sendMessageForm, {
|
||||
[css.sendMessageFormFocusedInMobileSafari]: isMobSaf && this.state.sendMessageFormFocused,
|
||||
});
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.container}>
|
||||
<div className={css.orderInfo}>
|
||||
<div className={css.imageWrapperMobile}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
rootClassName={css.rootForImage}
|
||||
|
|
@ -209,40 +232,82 @@ export const OrderDetailsPanelComponent = props => {
|
|||
{ name: 'landscape-crop', size: '400w' },
|
||||
{ name: 'landscape-crop2x', size: '800w' },
|
||||
]}
|
||||
sizes="100%"
|
||||
sizes="100vw"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.avatarWrapper}>
|
||||
<div className={classNames(css.avatarWrapper, css.avatarMobile)}>
|
||||
<AvatarMedium user={currentProvider} />
|
||||
</div>
|
||||
<div className={css.breakdownHeadings}>
|
||||
<h2 className={css.breakdownTitle}>{listingTitle}</h2>
|
||||
<p className={css.breakdownSubtitle}>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.hostedBy"
|
||||
values={{ name: authorDisplayName }}
|
||||
/>
|
||||
</p>
|
||||
<h1 className={css.heading}>{orderHeading}</h1>
|
||||
{showInfoMessage ? <p className={css.orderInfoText}>{orderInfoText}</p> : null}
|
||||
{showInfoMessage ? <hr className={css.infoTextDivider} /> : null}
|
||||
<div className={css.breakdownMobile}>
|
||||
<h3 className={css.bookingBreakdownTitle}>
|
||||
<FormattedMessage id="OrderDetailsPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
{bookingInfo}
|
||||
</div>
|
||||
{messagesContainer}
|
||||
<SendMessageForm
|
||||
form={sendMessageFormName}
|
||||
rootClassName={sendMessageFormClasses}
|
||||
messagePlaceholder={sendMessagePlaceholder}
|
||||
inProgress={sendMessageInProgress}
|
||||
sendMessageError={sendMessageError}
|
||||
onFocus={handleSendMessageFormFocus}
|
||||
onBlur={handleSendMessageFormBlur}
|
||||
onSubmit={handleMessageSubmit}
|
||||
/>
|
||||
</div>
|
||||
<div className={css.bookingBreakdownContainer}>
|
||||
<div className={css.breakdownDesktop}>
|
||||
<div className={css.breakdownImageWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
rootClassName={css.rootForImage}
|
||||
alt={listingTitle}
|
||||
image={firstImage}
|
||||
nameSet={[
|
||||
{ name: 'landscape-crop', size: '400w' },
|
||||
{ name: 'landscape-crop2x', size: '800w' },
|
||||
]}
|
||||
sizes="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.avatarWrapper}>
|
||||
<AvatarMedium user={currentProvider} />
|
||||
</div>
|
||||
<div className={css.breakdownHeadings}>
|
||||
<h2 className={css.breakdownTitle}>{listingTitle}</h2>
|
||||
<p className={css.breakdownSubtitle}>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.hostedBy"
|
||||
values={{ name: authorDisplayName }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<h3 className={css.bookingBreakdownTitle}>
|
||||
<FormattedMessage id="OrderDetailsPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
{bookingInfo}
|
||||
</div>
|
||||
<h3 className={css.bookingBreakdownTitle}>
|
||||
<FormattedMessage id="OrderDetailsPanel.bookingBreakdownTitle" />
|
||||
</h3>
|
||||
{bookingInfo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrderDetailsPanelComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
fetchMessagesError: null,
|
||||
sendMessageError: null,
|
||||
};
|
||||
|
||||
const { string, arrayOf, bool } = PropTypes;
|
||||
const { string, arrayOf, bool, func } = PropTypes;
|
||||
|
||||
OrderDetailsPanelComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
|
|
@ -253,6 +318,10 @@ OrderDetailsPanelComponent.propTypes = {
|
|||
messages: arrayOf(propTypes.message).isRequired,
|
||||
initialMessageFailed: bool.isRequired,
|
||||
fetchMessagesError: propTypes.error,
|
||||
sendMessageInProgress: bool.isRequired,
|
||||
sendMessageError: propTypes.error,
|
||||
onSendMessage: func.isRequired,
|
||||
onResetForm: func.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import { createBooking, createListing, createUser, createTransaction } from '../../util/test-data';
|
||||
import {
|
||||
createBooking,
|
||||
createListing,
|
||||
createUser,
|
||||
createTransaction,
|
||||
createCurrentUser,
|
||||
createMessage,
|
||||
} from '../../util/test-data';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
|
|
@ -57,11 +64,20 @@ const txDelivered = createTransaction({
|
|||
...baseTxAttrs,
|
||||
});
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
describe('OrderDetailsPanel', () => {
|
||||
const panelBaseProps = {
|
||||
intl: fakeIntl,
|
||||
messages: [],
|
||||
currentUser: createCurrentUser('user2'),
|
||||
messages: [
|
||||
createMessage('msg1', {}, { sender: createUser('user1') }),
|
||||
createMessage('msg2', {}, { sender: createUser('user2') }),
|
||||
],
|
||||
initialMessageFailed: false,
|
||||
sendMessageInProgress: false,
|
||||
onSendMessage: noop,
|
||||
onResetForm: noop,
|
||||
};
|
||||
|
||||
it('preauthorized matches snapshot', () => {
|
||||
|
|
|
|||
|
|
@ -211,6 +211,102 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
|
|||
userRole="customer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.messagesHeading"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
||||
<InjectIntl(Component)
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "user2@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "user2 abbreviated name",
|
||||
"displayName": "user2 display name",
|
||||
"firstName": "user2 first name",
|
||||
"lastName": "user2 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
messages={
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg1",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg2
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg2",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user2 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SendMessageForm
|
||||
form="OrderDetailsPanel.SendMessageForm"
|
||||
inProgress={false}
|
||||
messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSubmit={[Function]}
|
||||
rootClassName=""
|
||||
sendMessageError={null}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
|
@ -603,6 +699,102 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
|
|||
userRole="customer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.messagesHeading"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
||||
<InjectIntl(Component)
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "user2@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "user2 abbreviated name",
|
||||
"displayName": "user2 display name",
|
||||
"firstName": "user2 first name",
|
||||
"lastName": "user2 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
messages={
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg1",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg2
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg2",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user2 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SendMessageForm
|
||||
form="OrderDetailsPanel.SendMessageForm"
|
||||
inProgress={false}
|
||||
messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSubmit={[Function]}
|
||||
rootClassName=""
|
||||
sendMessageError={null}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
|
@ -1018,6 +1210,102 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
|
|||
userRole="customer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="undefined"
|
||||
>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.messagesHeading"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
||||
<InjectIntl(Component)
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "user2@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "user2 abbreviated name",
|
||||
"displayName": "user2 display name",
|
||||
"firstName": "user2 first name",
|
||||
"lastName": "user2 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
messages={
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg1",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg2
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg2",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user2 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SendMessageForm
|
||||
form="OrderDetailsPanel.SendMessageForm"
|
||||
inProgress={false}
|
||||
messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSubmit={[Function]}
|
||||
rootClassName=""
|
||||
sendMessageError={null}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
|
@ -1410,6 +1698,102 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = `
|
|||
userRole="customer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.messagesHeading"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
||||
<InjectIntl(Component)
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "user2@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "user2 abbreviated name",
|
||||
"displayName": "user2 display name",
|
||||
"firstName": "user2 first name",
|
||||
"lastName": "user2 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
messages={
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg1",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg2
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg2",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user2 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SendMessageForm
|
||||
form="OrderDetailsPanel.SendMessageForm"
|
||||
inProgress={false}
|
||||
messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSubmit={[Function]}
|
||||
rootClassName=""
|
||||
sendMessageError={null}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
|
@ -1802,6 +2186,102 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
|
|||
userRole="customer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.messagesHeading"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
||||
<InjectIntl(Component)
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "user2@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "user2 abbreviated name",
|
||||
"displayName": "user2 display name",
|
||||
"firstName": "user2 first name",
|
||||
"lastName": "user2 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
messages={
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg1",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg2
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg2",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user2 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SendMessageForm
|
||||
form="OrderDetailsPanel.SendMessageForm"
|
||||
inProgress={false}
|
||||
messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSubmit={[Function]}
|
||||
rootClassName=""
|
||||
sendMessageError={null}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
|
@ -2217,6 +2697,102 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
|
|||
userRole="customer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="undefined"
|
||||
>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="OrderDetailsPanel.messagesHeading"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
||||
<InjectIntl(Component)
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "user2@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "user2 abbreviated name",
|
||||
"displayName": "user2 display name",
|
||||
"firstName": "user2 first name",
|
||||
"lastName": "user2 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
messages={
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg1",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"at": 2017-11-09T08:12:00.000Z,
|
||||
"content": "Message msg2
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "msg2",
|
||||
},
|
||||
"sender": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user2 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user2",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"type": "message",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SendMessageForm
|
||||
form="OrderDetailsPanel.SendMessageForm"
|
||||
inProgress={false}
|
||||
messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSubmit={[Function]}
|
||||
rootClassName=""
|
||||
sendMessageError={null}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.mainContent {
|
||||
margin-bottom: 122px;
|
||||
@media (--viewportLarge) {
|
||||
margin-bottom: 122px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
|
@ -26,3 +28,11 @@
|
|||
.activeTab {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: none;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ export const FETCH_MESSAGES_REQUEST = 'app/OrderPage/FETCH_MESSAGES_REQUEST';
|
|||
export const FETCH_MESSAGES_SUCCESS = 'app/OrderPage/FETCH_MESSAGES_SUCCESS';
|
||||
export const FETCH_MESSAGES_ERROR = 'app/OrderPage/FETCH_MESSAGES_ERROR';
|
||||
|
||||
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';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
|
|
@ -26,6 +30,8 @@ const initialState = {
|
|||
fetchMessagesError: null,
|
||||
messages: [],
|
||||
messageSendingFailedToTransaction: null,
|
||||
sendMessageInProgress: false,
|
||||
sendMessageError: null,
|
||||
};
|
||||
|
||||
export default function checkoutPageReducer(state = initialState, action = {}) {
|
||||
|
|
@ -43,6 +49,7 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
|
|||
case FETCH_ORDER_ERROR:
|
||||
console.error(payload); // eslint-disable-line
|
||||
return { ...state, fetchOrderInProgress: false, fetchOrderError: payload };
|
||||
|
||||
case FETCH_MESSAGES_REQUEST:
|
||||
return { ...state, fetchMessagesInProgress: true, fetchMessagesError: null };
|
||||
case FETCH_MESSAGES_SUCCESS:
|
||||
|
|
@ -50,6 +57,13 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
|
|||
case FETCH_MESSAGES_ERROR:
|
||||
return { ...state, fetchMessagesInProgress: false, fetchMessagesError: payload };
|
||||
|
||||
case SEND_MESSAGE_REQUEST:
|
||||
return { ...state, sendMessageInProgress: true, sendMessageError: null };
|
||||
case SEND_MESSAGE_SUCCESS:
|
||||
return { ...state, sendMessageInProgress: false };
|
||||
case SEND_MESSAGE_ERROR:
|
||||
return { ...state, sendMessageInProgress: false, sendMessageError: payload };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -70,6 +84,10 @@ const fetchMessagesRequest = () => ({ type: FETCH_MESSAGES_REQUEST });
|
|||
const fetchMessagesSuccess = messages => ({ type: FETCH_MESSAGES_SUCCESS, payload: messages });
|
||||
const fetchMessagesError = e => ({ type: FETCH_MESSAGES_ERROR, error: true, payload: e });
|
||||
|
||||
const sendMessageRequest = () => ({ type: SEND_MESSAGE_REQUEST });
|
||||
const sendMessageSuccess = () => ({ type: SEND_MESSAGE_SUCCESS });
|
||||
const sendMessageError = e => ({ type: SEND_MESSAGE_ERROR, error: true, payload: e });
|
||||
|
||||
// ================ Thunks ================ //
|
||||
|
||||
const listingRelationship = txResponse => {
|
||||
|
|
@ -130,7 +148,7 @@ export const fetchMessages = txId => (dispatch, getState, sdk) => {
|
|||
dispatch(fetchMessagesSuccess(denormalized));
|
||||
})
|
||||
.catch(e => {
|
||||
dispatch(fetchMessagesError(e));
|
||||
dispatch(fetchMessagesError(storableError(e)));
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
|
@ -140,6 +158,31 @@ export const fetchMessages = txId => (dispatch, getState, sdk) => {
|
|||
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(fetchMessages(orderId))]);
|
||||
};
|
||||
|
||||
export const sendMessage = (orderId, message) => (dispatch, getState, sdk) => {
|
||||
dispatch(sendMessageRequest());
|
||||
|
||||
return sdk.messages
|
||||
.send({ transactionId: orderId, content: message })
|
||||
.then(response => {
|
||||
const messageId = response.data.data.id;
|
||||
return dispatch(fetchMessages(orderId))
|
||||
.then(() => {
|
||||
dispatch(sendMessageSuccess());
|
||||
return messageId;
|
||||
})
|
||||
.catch(() => dispatch(sendMessageSuccess()));
|
||||
})
|
||||
.catch(e => {
|
||||
dispatch(sendMessageError(storableError(e)));
|
||||
// Rethrow so the page can track whether the sending failed, and
|
||||
// keep the message in the form for a retry.
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { reset as resetForm } from 'redux-form';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
|
|
@ -20,7 +21,7 @@ import {
|
|||
} from '../../components';
|
||||
import { TopbarContainer } from '../../containers';
|
||||
|
||||
import { loadData, setInitialValues } from './OrderPage.duck';
|
||||
import { loadData, setInitialValues, sendMessage } from './OrderPage.duck';
|
||||
import css from './OrderPage.css';
|
||||
|
||||
// OrderPage handles data loading
|
||||
|
|
@ -32,6 +33,10 @@ export const OrderPageComponent = props => {
|
|||
fetchMessagesError,
|
||||
messages,
|
||||
messageSendingFailedToTransaction,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
onSendMessage,
|
||||
onResetForm,
|
||||
intl,
|
||||
params,
|
||||
scrollingDisabled,
|
||||
|
|
@ -84,6 +89,10 @@ export const OrderPageComponent = props => {
|
|||
messages={messages}
|
||||
initialMessageFailed={initialMessageFailed}
|
||||
fetchMessagesError={fetchMessagesError}
|
||||
sendMessageInProgress={sendMessageInProgress}
|
||||
sendMessageError={sendMessageError}
|
||||
onSendMessage={onSendMessage}
|
||||
onResetForm={onResetForm}
|
||||
/>
|
||||
) : (
|
||||
loadingOrFaildFetching
|
||||
|
|
@ -100,7 +109,7 @@ export const OrderPageComponent = props => {
|
|||
</LayoutWrapperTopbar>
|
||||
<LayoutWrapperMain className={css.mainContent}>{panel}</LayoutWrapperMain>
|
||||
<LayoutWrapperFooter>
|
||||
<Footer />
|
||||
<Footer className={css.footer} />
|
||||
</LayoutWrapperFooter>
|
||||
</LayoutSingleColumn>
|
||||
</Page>
|
||||
|
|
@ -112,10 +121,11 @@ OrderPageComponent.defaultProps = {
|
|||
fetchOrderError: null,
|
||||
fetchMessagesError: null,
|
||||
messageSendingFailedToTransaction: null,
|
||||
sendMessageError: null,
|
||||
transaction: null,
|
||||
};
|
||||
|
||||
const { bool, oneOf, shape, string, array } = PropTypes;
|
||||
const { bool, oneOf, shape, string, array, func } = PropTypes;
|
||||
|
||||
OrderPageComponent.propTypes = {
|
||||
params: shape({ id: string }).isRequired,
|
||||
|
|
@ -126,8 +136,12 @@ OrderPageComponent.propTypes = {
|
|||
fetchMessagesError: propTypes.error,
|
||||
messages: array.isRequired,
|
||||
messageSendingFailedToTransaction: propTypes.uuid,
|
||||
sendMessageInProgress: bool.isRequired,
|
||||
sendMessageError: propTypes.error,
|
||||
scrollingDisabled: bool.isRequired,
|
||||
transaction: propTypes.transaction,
|
||||
onSendMessage: func.isRequired,
|
||||
onResetForm: func.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
|
|
@ -141,6 +155,8 @@ const mapStateToProps = state => {
|
|||
fetchMessagesError,
|
||||
messages,
|
||||
messageSendingFailedToTransaction,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
} = state.OrderPage;
|
||||
const transactions = getMarketplaceEntities(state, transactionRef ? [transactionRef] : []);
|
||||
const transaction = transactions.length > 0 ? transactions[0] : null;
|
||||
|
|
@ -151,12 +167,21 @@ const mapStateToProps = state => {
|
|||
fetchMessagesError,
|
||||
messages,
|
||||
messageSendingFailedToTransaction,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
scrollingDisabled: isScrollingDisabled(state),
|
||||
transaction,
|
||||
};
|
||||
};
|
||||
|
||||
const OrderPage = compose(connect(mapStateToProps), injectIntl)(OrderPageComponent);
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSendMessage: (orderId, message) => dispatch(sendMessage(orderId, message)),
|
||||
onResetForm: formName => dispatch(resetForm(formName)),
|
||||
});
|
||||
|
||||
const OrderPage = compose(connect(mapStateToProps, mapDispatchToProps), injectIntl)(
|
||||
OrderPageComponent
|
||||
);
|
||||
|
||||
OrderPage.setInitialValues = setInitialValues;
|
||||
OrderPage.loadData = loadData;
|
||||
|
|
|
|||
|
|
@ -34,8 +34,11 @@ describe('OrderPage', () => {
|
|||
|
||||
currentUser: createCurrentUser('customer1'),
|
||||
messages: [],
|
||||
sendMessageInProgress: false,
|
||||
scrollingDisabled: false,
|
||||
transaction,
|
||||
onSendMessage: noop,
|
||||
onResetForm: noop,
|
||||
|
||||
intl: fakeIntl,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ exports[`OrderPage matches snapshot 1`] = `
|
|||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<InjectIntl(Component)
|
||||
<InjectIntl(OrderDetailsPanelComponent)
|
||||
className="undefined"
|
||||
currentUser={
|
||||
Object {
|
||||
|
|
@ -44,6 +44,10 @@ exports[`OrderPage matches snapshot 1`] = `
|
|||
fetchMessagesError={null}
|
||||
initialMessageFailed={false}
|
||||
messages={Array []}
|
||||
onResetForm={[Function]}
|
||||
onSendMessage={[Function]}
|
||||
sendMessageError={null}
|
||||
sendMessageInProgress={false}
|
||||
transaction={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
|
|
|
|||
|
|
@ -40,18 +40,32 @@
|
|||
|
||||
.submitButtonMobile {
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
right: 20px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
top: -21px;
|
||||
right: 24px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background-color: var(--successColor);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
stroke: var(--matterColorLight);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.fillSuccess {
|
||||
fill: var(--successColor);
|
||||
}
|
||||
|
|
@ -60,14 +74,61 @@
|
|||
stroke: var(--matterColor);
|
||||
}
|
||||
|
||||
.errorMobile {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
color: var(--failColor);
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
right: 80px;
|
||||
padding: 5px 16px;
|
||||
background-color: var(--matterColorLight);
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 0;
|
||||
top: -14px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.submitContainerDesktop {
|
||||
display: none;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.errorContainerDesktop {
|
||||
display: none;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
padding: 26px 24px 0 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.errorDesktop {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
color: var(--failColor);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.submitButtonDesktop {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
display: none;
|
||||
|
||||
float: right;
|
||||
padding: 0 16px;
|
||||
min-height: auto;
|
||||
min-width: 150px;
|
||||
height: 41px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
|
|
@ -77,6 +138,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sendIconMobile {
|
||||
margin: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.sendIconMobileInProgress {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: var(--matterColorLight);
|
||||
stroke-width: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sendIconDesktop {
|
||||
margin: -3px 5px 0 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import SendMessageForm from './SendMessageForm';
|
|||
export const Empty = {
|
||||
component: SendMessageForm,
|
||||
props: {
|
||||
form: 'Styleguide_SendMessageForm_Empty',
|
||||
messagePlaceholder: 'Send message to Juho…',
|
||||
onChange: values => {
|
||||
console.log('values changed to:', values);
|
||||
|
|
@ -19,3 +20,23 @@ export const Empty = {
|
|||
},
|
||||
group: 'forms',
|
||||
};
|
||||
|
||||
export const InProgress = {
|
||||
component: SendMessageForm,
|
||||
props: {
|
||||
form: 'Styleguide_SendMessageForm_InProgress',
|
||||
messagePlaceholder: 'Send message to Juho…',
|
||||
inProgress: true,
|
||||
},
|
||||
group: 'forms',
|
||||
};
|
||||
|
||||
export const Error = {
|
||||
component: SendMessageForm,
|
||||
props: {
|
||||
form: 'Styleguide_SendMessageForm_Error',
|
||||
messagePlaceholder: 'Send message to Juho…',
|
||||
sendMessageError: { type: 'error', name: 'ExampleError' },
|
||||
},
|
||||
group: 'forms',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { compose } from 'redux';
|
|||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
import classNames from 'classnames';
|
||||
import { Form, TextInputField, SecondaryButton } from '../../components';
|
||||
import { Form, TextInputField, SecondaryButton, IconSpinner } from '../../components';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
|
||||
import css from './SendMessageForm.css';
|
||||
|
||||
|
|
@ -12,7 +13,13 @@ const BLUR_TIMEOUT_MS = 100;
|
|||
|
||||
const IconSendMessageMobile = () => {
|
||||
return (
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
className={css.sendIconMobile}
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 26 26"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
x="-1.9%"
|
||||
|
|
@ -35,11 +42,8 @@ const IconSendMessageMobile = () => {
|
|||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(4 6)" filter="url(#a)" fill="none" fillRule="evenodd">
|
||||
<rect className={css.fillSuccess} width="48" height="48" rx="24" />
|
||||
<g fill="#FFF">
|
||||
<path d="M14.47 23.048c-.14.05-.237.193-.25.36-.013.163.062.317.19.39l4.623 2.688 12.162-10.593-16.726 7.155zM20.47 27.327l-.97 6.59c0 .228.184.416.417.416.145 0 .284-.076.36-.206l2.94-4.823 4.833 2.894c.118.066.26.067.373.015.12-.055.207-.162.234-.292l3.315-15.328-11.5 10.735z" />
|
||||
</g>
|
||||
<g filter="url(#a)" transform="translate(-313 -10)" fill="#FFF" fillRule="evenodd">
|
||||
<path d="M317.47 23.048c-.14.05-.237.193-.25.36-.013.163.062.317.19.39l4.623 2.688 12.162-10.593-16.726 7.155zM323.47 27.327l-.97 6.59c0 .228.184.416.417.416.145 0 .284-.076.36-.206l2.94-4.823 4.833 2.894c.118.066.26.067.373.015.12-.055.207-.162.234-.292l3.315-15.328-11.5 10.735z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -92,6 +96,7 @@ class SendMessageFormComponent extends Component {
|
|||
handleSubmit,
|
||||
submitting,
|
||||
inProgress,
|
||||
sendMessageError,
|
||||
invalid,
|
||||
} = this.props;
|
||||
|
||||
|
|
@ -110,19 +115,37 @@ class SendMessageFormComponent extends Component {
|
|||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
/>
|
||||
<button className={css.submitButtonMobile}>
|
||||
<IconSendMessageMobile />
|
||||
{sendMessageError ? (
|
||||
<p className={css.errorMobile}>
|
||||
<FormattedMessage id="SendMessageForm.sendFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
<button className={css.submitButtonMobile} disabled={submitDisabled}>
|
||||
{submitInProgress ? (
|
||||
<IconSpinner className={css.sendIconMobileInProgress} />
|
||||
) : (
|
||||
<IconSendMessageMobile />
|
||||
)}
|
||||
</button>
|
||||
<SecondaryButton
|
||||
className={css.submitButtonDesktop}
|
||||
inProgress={submitInProgress}
|
||||
disabled={submitDisabled}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
<IconSendMessageDesktop />
|
||||
<FormattedMessage id="SendMessageForm.sendMessage" />
|
||||
</SecondaryButton>
|
||||
<div className={css.submitContainerDesktop}>
|
||||
<div className={css.errorContainerDesktop}>
|
||||
{sendMessageError ? (
|
||||
<p className={css.errorDesktop}>
|
||||
<FormattedMessage id="SendMessageForm.sendFailed" />
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<SecondaryButton
|
||||
className={css.submitButtonDesktop}
|
||||
inProgress={submitInProgress}
|
||||
disabled={submitDisabled}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
<IconSendMessageDesktop />
|
||||
<FormattedMessage id="SendMessageForm.sendMessage" />
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -135,6 +158,7 @@ SendMessageFormComponent.defaultProps = {
|
|||
messagePlaceholder: null,
|
||||
onFocus: () => null,
|
||||
onBlur: () => null,
|
||||
sendMessageError: null,
|
||||
};
|
||||
|
||||
SendMessageFormComponent.propTypes = {
|
||||
|
|
@ -146,6 +170,7 @@ SendMessageFormComponent.propTypes = {
|
|||
messagePlaceholder: string,
|
||||
onFocus: func,
|
||||
onBlur: func,
|
||||
sendMessageError: propTypes.error,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
|
|
@ -157,4 +182,6 @@ const SendMessageForm = compose(reduxForm({ form: defaultFormName }), injectIntl
|
|||
SendMessageFormComponent
|
||||
);
|
||||
|
||||
SendMessageForm.displayName = 'SendMessageForm';
|
||||
|
||||
export default SendMessageForm;
|
||||
|
|
|
|||
|
|
@ -272,6 +272,7 @@
|
|||
"OrderDetailsPanel.orderPreauthorizedStatus": "{providerName} has been notified about the booking request, so sit back and relax.",
|
||||
"OrderDetailsPanel.orderPreauthorizedSubtitle": "You have requested to book {listingLink}",
|
||||
"OrderDetailsPanel.orderPreauthorizedTitle": "Great success, {customerName}!",
|
||||
"OrderDetailsPanel.sendMessagePlaceholder": "Send a message to {name}…",
|
||||
"OrderPage.fetchOrderFailed": "Fetching order data failed.",
|
||||
"OrderPage.loadingData": "Loading order data.",
|
||||
"OrderPage.title": "Order details: {listingTitle}",
|
||||
|
|
@ -470,6 +471,7 @@
|
|||
"SectionLocations.listingsInLocation": "Saunas in {location}",
|
||||
"SectionLocations.subtitle": "We have more than 1000 saunas around Finland. Here are some of our most popular locations.",
|
||||
"SectionLocations.title": "We have wooden saunas, electric saunas and even tent saunas.",
|
||||
"SendMessageForm.sendFailed": "Failed to send. Please try again.",
|
||||
"SendMessageForm.sendMessage": "Send message",
|
||||
"SignupForm.emailInvalid": "A valid email address is required",
|
||||
"SignupForm.emailLabel": "Email",
|
||||
|
|
|
|||
|
|
@ -62,12 +62,13 @@ export const createResourceLocatorString = (
|
|||
routeName,
|
||||
routes,
|
||||
pathParams = {},
|
||||
searchParams = {}
|
||||
searchParams = {},
|
||||
hash = ''
|
||||
) => {
|
||||
const searchQuery = stringify(searchParams);
|
||||
const includeSearchQuery = searchQuery.length > 0 ? `?${searchQuery}` : '';
|
||||
const path = pathByRouteName(routeName, routes, pathParams);
|
||||
return `${path}${includeSearchQuery}`;
|
||||
return `${path}${includeSearchQuery}${hash}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
14
src/util/userAgent.js
Normal file
14
src/util/userAgent.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export const isMobileSafari = () => {
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/29696509
|
||||
const ua = window.navigator.userAgent;
|
||||
const iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i);
|
||||
const webkit = !!ua.match(/WebKit/i);
|
||||
|
||||
// If iOS Chrome needs to be separated, use `!ua.match(/CriOS/i)` as
|
||||
// an extra condition.
|
||||
return iOS && webkit;
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue