From 9a13d8193a84ec03f539a053ecaea1d794911ee9 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Tue, 14 Nov 2017 10:46:07 +0200 Subject: [PATCH 1/7] Render SendMessageForm in Order page --- .../OrderDetailsPanel/OrderDetailsPanel.css | 31 ++++++++++++++----- .../OrderDetailsPanel/OrderDetailsPanel.js | 13 ++++++++ .../OrderDetailsPanel.test.js.snap | 18 +++++++++++ src/containers/OrderPage/OrderPage.css | 12 ++++++- src/containers/OrderPage/OrderPage.js | 2 +- .../SendMessageForm/SendMessageForm.js | 2 ++ src/translations/en.json | 1 + 7 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.css b/src/components/OrderDetailsPanel/OrderDetailsPanel.css index 43a0abef..64817db2 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.css +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.css @@ -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,16 @@ margin-top: 19px; } } + +.sendMessageForm { + position: fixed; + bottom: 0; + width: 100%; + box-shadow: var(--boxShadowBottomForm); + + @media (--viewportLarge) { + margin-top: 47px; + position: relative; + box-shadow: none; + } +} diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js index d728d2e7..95dc0591 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.js @@ -12,6 +12,7 @@ import { AvatarMedium, Messages, } from '../../components'; +import { SendMessageForm } from '../../containers'; import css from './OrderDetailsPanel.css'; @@ -163,6 +164,17 @@ export const OrderDetailsPanelComponent = props => { ) : null; + const sendMessagePlaceholder = intl.formatMessage( + { id: 'OrderDetailsPanel.sendMessagePlaceholder' }, + { name: 'Juho' } + ); + const sendMessageForm = ( + + ); + const classes = classNames(rootClassName || css.root, className); return ( @@ -196,6 +208,7 @@ export const OrderDetailsPanelComponent = props => { {bookingInfo} {messagesContainer} + {sendMessageForm}
diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap index add241f4..5c7e55e3 100644 --- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap +++ b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap @@ -211,6 +211,9 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` userRole="customer" />
+
@@ -603,6 +606,9 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` userRole="customer" />
+
@@ -1018,6 +1024,9 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` userRole="customer" />
+
@@ -1410,6 +1419,9 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` userRole="customer" />
+
@@ -1802,6 +1814,9 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` userRole="customer" />
+
@@ -2217,6 +2232,9 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` userRole="customer" />
+
diff --git a/src/containers/OrderPage/OrderPage.css b/src/containers/OrderPage/OrderPage.css index fa25c1b0..783de09e 100644 --- a/src/containers/OrderPage/OrderPage.css +++ b/src/containers/OrderPage/OrderPage.css @@ -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; + } +} diff --git a/src/containers/OrderPage/OrderPage.js b/src/containers/OrderPage/OrderPage.js index 25aa2416..24f9c388 100644 --- a/src/containers/OrderPage/OrderPage.js +++ b/src/containers/OrderPage/OrderPage.js @@ -100,7 +100,7 @@ export const OrderPageComponent = props => { {panel} -
+
diff --git a/src/containers/SendMessageForm/SendMessageForm.js b/src/containers/SendMessageForm/SendMessageForm.js index 3da792eb..2bd150f2 100644 --- a/src/containers/SendMessageForm/SendMessageForm.js +++ b/src/containers/SendMessageForm/SendMessageForm.js @@ -157,4 +157,6 @@ const SendMessageForm = compose(reduxForm({ form: defaultFormName }), injectIntl SendMessageFormComponent ); +SendMessageForm.displayName = 'SendMessageForm'; + export default SendMessageForm; diff --git a/src/translations/en.json b/src/translations/en.json index bf73fe6a..7e7b834d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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}", From ff108ca489dd8d7c403e1ac32c1fca9f6d95207a Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Tue, 14 Nov 2017 14:33:51 +0200 Subject: [PATCH 2/7] Add in progress and error states --- .../SendMessageForm/SendMessageForm.css | 83 +++++++++++++++++-- .../SendMessageForm.example.js | 21 +++++ .../SendMessageForm/SendMessageForm.js | 65 ++++++++++----- src/translations/en.json | 1 + 4 files changed, 146 insertions(+), 24 deletions(-) diff --git a/src/containers/SendMessageForm/SendMessageForm.css b/src/containers/SendMessageForm/SendMessageForm.css index e9062c41..88b0e87a 100644 --- a/src/containers/SendMessageForm/SendMessageForm.css +++ b/src/containers/SendMessageForm/SendMessageForm.css @@ -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; } diff --git a/src/containers/SendMessageForm/SendMessageForm.example.js b/src/containers/SendMessageForm/SendMessageForm.example.js index d0a65c8f..e1a8cb79 100644 --- a/src/containers/SendMessageForm/SendMessageForm.example.js +++ b/src/containers/SendMessageForm/SendMessageForm.example.js @@ -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', +}; diff --git a/src/containers/SendMessageForm/SendMessageForm.js b/src/containers/SendMessageForm/SendMessageForm.js index 2bd150f2..510615db 100644 --- a/src/containers/SendMessageForm/SendMessageForm.js +++ b/src/containers/SendMessageForm/SendMessageForm.js @@ -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 ( - + { - - - - - + + ); @@ -92,9 +96,12 @@ class SendMessageFormComponent extends Component { handleSubmit, submitting, inProgress, + sendMessageError, invalid, } = this.props; + console.log({ sendMessageError }); + const classes = classNames(rootClassName || css.root, className); const submitInProgress = submitting || inProgress; const submitDisabled = invalid || submitInProgress; @@ -110,19 +117,37 @@ class SendMessageFormComponent extends Component { onFocus={this.handleFocus} onBlur={this.handleBlur} /> -

+ +

+ ) : null} + - - - - +
+
+ {sendMessageError ? ( +

+ +

+ ) : null} +
+ + + + +
); } @@ -135,6 +160,7 @@ SendMessageFormComponent.defaultProps = { messagePlaceholder: null, onFocus: () => null, onBlur: () => null, + sendMessageError: null, }; SendMessageFormComponent.propTypes = { @@ -146,6 +172,7 @@ SendMessageFormComponent.propTypes = { messagePlaceholder: string, onFocus: func, onBlur: func, + sendMessageError: propTypes.error, // from injectIntl intl: intlShape.isRequired, diff --git a/src/translations/en.json b/src/translations/en.json index 7e7b834d..69095279 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -471,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", From a3ca212a247e07fda0cef2b01f3596c4aee77820 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Tue, 14 Nov 2017 16:24:13 +0200 Subject: [PATCH 3/7] Send messages from Order page --- src/components/IconSpinner/IconSpinner.css | 1 + src/components/IconSpinner/IconSpinner.js | 1 - src/components/Messages/Messages.js | 2 +- .../OrderDetailsPanel/OrderDetailsPanel.js | 331 ++++++++++-------- src/containers/OrderPage/OrderPage.duck.js | 46 ++- src/containers/OrderPage/OrderPage.js | 31 +- .../SendMessageForm/SendMessageForm.js | 2 - src/util/routes.js | 5 +- 8 files changed, 263 insertions(+), 156 deletions(-) diff --git a/src/components/IconSpinner/IconSpinner.css b/src/components/IconSpinner/IconSpinner.css index ee46c953..89daeb92 100644 --- a/src/components/IconSpinner/IconSpinner.css +++ b/src/components/IconSpinner/IconSpinner.css @@ -8,4 +8,5 @@ height: 28px; stroke: var(--marketplaceColor); + stroke-width: 3px; } diff --git a/src/components/IconSpinner/IconSpinner.js b/src/components/IconSpinner/IconSpinner.js index 3e633a45..c44b9c2f 100644 --- a/src/components/IconSpinner/IconSpinner.js +++ b/src/components/IconSpinner/IconSpinner.js @@ -11,7 +11,6 @@ const IconSpinner = props => { diff --git a/src/components/Messages/Messages.js b/src/components/Messages/Messages.js index cf622cba..bb490380 100644 --- a/src/components/Messages/Messages.js +++ b/src/components/Messages/Messages.js @@ -63,7 +63,7 @@ export const MessagesComponent = props => { return (
    {messages.map(m => ( -
  • +
  • {msg(m)}
  • ))} diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js index 95dc0591..220969ca 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.js @@ -1,4 +1,4 @@ -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'; @@ -78,141 +78,136 @@ 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 = ( - - {title} - - ); - } else { - listingLink = deletedListingOrderTitle; +export class OrderDetailsPanelComponent extends Component { + constructor(props) { + super(props); + this.state = { setInitialEmptyMessage: 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 ? ( -
    -

    - -

    - {initialMessageFailed ? ( -

    - -

    - ) : null} - {fetchMessagesError ? ( -

    - -

    - ) : null} - -
    - ) : null; + const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName); + const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName); - const sendMessagePlaceholder = intl.formatMessage( - { id: 'OrderDetailsPanel.sendMessagePlaceholder' }, - { name: 'Juho' } - ); - const sendMessageForm = ( - - ); + let listingLink = null; - const classes = classNames(rootClassName || css.root, className); + if (listingLoaded && currentListing.attributes.title) { + const title = currentListing.attributes.title; + const params = { id: currentListing.id.uuid, slug: createSlug(title) }; + listingLink = ( + + {title} + + ); + } else { + listingLink = deletedListingOrderTitle; + } - return ( -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -

    {orderHeading}

    - {showInfoMessage ?

    {orderInfoText}

    : null} - {showInfoMessage ?
    : null} -
    -

    - -

    - {bookingInfo} -
    - {messagesContainer} - {sendMessageForm} -
    -
    -
    -
    + 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 ? ( +
    +

    + +

    + {initialMessageFailed ? ( +

    + +

    + ) : null} + {fetchMessagesError ? ( +

    + +

    + ) : null} + +
    + ) : 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 handleMessageSubmit = values => { + return onSendMessage(currentTransaction.id, values.message) + .then(messageId => { + onResetForm(sendMessageFormName); + scrollToMessage(messageId); + }) + .catch(e => { + console.error(e); + }); + }; + + const classes = classNames(rootClassName || css.root, className); + + return ( +
    +
    +
    +
    { { name: 'landscape-crop', size: '400w' }, { name: 'landscape-crop2x', size: '800w' }, ]} - sizes="100%" + sizes="100vw" />
    -
    +
    -
    -

    {listingTitle}

    -

    - -

    +

    {orderHeading}

    + {showInfoMessage ?

    {orderInfoText}

    : null} + {showInfoMessage ?
    : null} +
    +

    + +

    + {bookingInfo} +
    + {messagesContainer} + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +

    {listingTitle}

    +

    + +

    +
    +

    + +

    + {bookingInfo}
    -

    - -

    - {bookingInfo}
    -
    - ); -}; + ); + } +} 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, @@ -266,6 +301,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, diff --git a/src/containers/OrderPage/OrderPage.duck.js b/src/containers/OrderPage/OrderPage.duck.js index 2a715359..e4e082de 100644 --- a/src/containers/OrderPage/OrderPage.duck.js +++ b/src/containers/OrderPage/OrderPage.duck.js @@ -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,32 @@ 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) => { + console.log('sendMessage:', orderId, message); + 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; + }); +}; diff --git a/src/containers/OrderPage/OrderPage.js b/src/containers/OrderPage/OrderPage.js index 24f9c388..03477a41 100644 --- a/src/containers/OrderPage/OrderPage.js +++ b/src/containers/OrderPage/OrderPage.js @@ -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 @@ -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; diff --git a/src/containers/SendMessageForm/SendMessageForm.js b/src/containers/SendMessageForm/SendMessageForm.js index 510615db..d4d08f43 100644 --- a/src/containers/SendMessageForm/SendMessageForm.js +++ b/src/containers/SendMessageForm/SendMessageForm.js @@ -100,8 +100,6 @@ class SendMessageFormComponent extends Component { invalid, } = this.props; - console.log({ sendMessageError }); - const classes = classNames(rootClassName || css.root, className); const submitInProgress = submitting || inProgress; const submitDisabled = invalid || submitInProgress; diff --git a/src/util/routes.js b/src/util/routes.js index aa805672..f0ada179 100644 --- a/src/util/routes.js +++ b/src/util/routes.js @@ -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}`; }; /** From 444de6e743c668bfc0bfa154b1a4ed646e6cf976 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Wed, 15 Nov 2017 10:37:02 +0200 Subject: [PATCH 4/7] Fix changed props in tests --- .../__snapshots__/Messages.test.js.snap | 2 + .../OrderDetailsPanel.test.js | 20 +- .../OrderDetailsPanel.test.js.snap | 540 ++++++++++++++++++ src/containers/OrderPage/OrderPage.test.js | 3 + .../__snapshots__/OrderPage.test.js.snap | 6 +- 5 files changed, 568 insertions(+), 3 deletions(-) diff --git a/src/components/Messages/__snapshots__/Messages.test.js.snap b/src/components/Messages/__snapshots__/Messages.test.js.snap index b054976d..05ef3aac 100644 --- a/src/components/Messages/__snapshots__/Messages.test.js.snap +++ b/src/components/Messages/__snapshots__/Messages.test.js.snap @@ -6,6 +6,7 @@ exports[`Messages matches snapshot 1`] = ` >
  • 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', () => { diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap index 5c7e55e3..7566cd1d 100644 --- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap +++ b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap @@ -211,8 +211,98 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` userRole="customer" />
    +
    +

    + +

    + +
  • @@ -606,8 +696,98 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` userRole="customer" />
    +
    +

    + +

    + +
    @@ -1024,8 +1204,98 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` userRole="customer" />
    +
    +

    + +

    + +
    @@ -1419,8 +1689,98 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` userRole="customer" />
    +
    +

    + +

    + +
    @@ -1814,8 +2174,98 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` userRole="customer" />
    +
    +

    + +

    + +
    @@ -2232,8 +2682,98 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` userRole="customer" />
    +
    +

    + +

    + +
    diff --git a/src/containers/OrderPage/OrderPage.test.js b/src/containers/OrderPage/OrderPage.test.js index e01e2c4b..3d5c74ad 100644 --- a/src/containers/OrderPage/OrderPage.test.js +++ b/src/containers/OrderPage/OrderPage.test.js @@ -34,8 +34,11 @@ describe('OrderPage', () => { currentUser: createCurrentUser('customer1'), messages: [], + sendMessageInProgress: false, scrollingDisabled: false, transaction, + onSendMessage: noop, + onResetForm: noop, intl: fakeIntl, }; diff --git a/src/containers/OrderPage/__snapshots__/OrderPage.test.js.snap b/src/containers/OrderPage/__snapshots__/OrderPage.test.js.snap index 4fb96448..2bcba8a0 100644 --- a/src/containers/OrderPage/__snapshots__/OrderPage.test.js.snap +++ b/src/containers/OrderPage/__snapshots__/OrderPage.test.js.snap @@ -19,7 +19,7 @@ exports[`OrderPage matches snapshot 1`] = ` className={null} rootClassName={null} > - Date: Wed, 15 Nov 2017 10:50:56 +0200 Subject: [PATCH 5/7] Remove console.log call --- src/containers/OrderPage/OrderPage.duck.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/containers/OrderPage/OrderPage.duck.js b/src/containers/OrderPage/OrderPage.duck.js index e4e082de..81c0229b 100644 --- a/src/containers/OrderPage/OrderPage.duck.js +++ b/src/containers/OrderPage/OrderPage.duck.js @@ -166,7 +166,6 @@ export const loadData = params => dispatch => { }; export const sendMessage = (orderId, message) => (dispatch, getState, sdk) => { - console.log('sendMessage:', orderId, message); dispatch(sendMessageRequest()); return sdk.messages From 8fcb6042e6b9d051be42d38131abfd1e18e913a4 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Wed, 15 Nov 2017 11:38:38 +0200 Subject: [PATCH 6/7] Fix fixed form in Mobile Safari --- .../OrderDetailsPanel/OrderDetailsPanel.css | 8 +++++++ .../OrderDetailsPanel/OrderDetailsPanel.js | 21 +++++++++++++++++-- .../OrderDetailsPanel.test.js.snap | 18 ++++++++++++++++ src/util/userAgent.js | 14 +++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/util/userAgent.js diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.css b/src/components/OrderDetailsPanel/OrderDetailsPanel.css index 64817db2..a507dec4 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.css +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.css @@ -279,3 +279,11 @@ box-shadow: none; } } + +.sendMessageFormFocusedInMobileSafari { + position: absolute; + + @media (--viewportLarge) { + position: relative; + } +} diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js index 220969ca..4f073b12 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.js @@ -5,6 +5,7 @@ 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, @@ -81,7 +82,7 @@ const orderMessage = (transaction, providerName) => { export class OrderDetailsPanelComponent extends Component { constructor(props) { super(props); - this.state = { setInitialEmptyMessage: false }; + this.state = { sendMessageFormFocused: false }; } render() { const { @@ -190,6 +191,17 @@ export class OrderDetailsPanelComponent extends Component { }); } }; + 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 => { @@ -200,6 +212,9 @@ export class OrderDetailsPanelComponent extends Component { console.error(e); }); }; + const sendMessageFormClasses = classNames(css.sendMessageForm, { + [css.sendMessageFormFocusedInMobileSafari]: isMobSaf && this.state.sendMessageFormFocused, + }); const classes = classNames(rootClassName || css.root, className); @@ -236,10 +251,12 @@ export class OrderDetailsPanelComponent extends Component { {messagesContainer}
    diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap index 7566cd1d..55b6410e 100644 --- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap +++ b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap @@ -301,7 +301,10 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` form="OrderDetailsPanel.SendMessageForm" inProgress={false} messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder" + onBlur={[Function]} + onFocus={[Function]} onSubmit={[Function]} + rootClassName="" sendMessageError={null} />
@@ -786,7 +789,10 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` form="OrderDetailsPanel.SendMessageForm" inProgress={false} messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder" + onBlur={[Function]} + onFocus={[Function]} onSubmit={[Function]} + rootClassName="" sendMessageError={null} />
@@ -1294,7 +1300,10 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` form="OrderDetailsPanel.SendMessageForm" inProgress={false} messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder" + onBlur={[Function]} + onFocus={[Function]} onSubmit={[Function]} + rootClassName="" sendMessageError={null} /> @@ -1779,7 +1788,10 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` form="OrderDetailsPanel.SendMessageForm" inProgress={false} messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder" + onBlur={[Function]} + onFocus={[Function]} onSubmit={[Function]} + rootClassName="" sendMessageError={null} /> @@ -2264,7 +2276,10 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` form="OrderDetailsPanel.SendMessageForm" inProgress={false} messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder" + onBlur={[Function]} + onFocus={[Function]} onSubmit={[Function]} + rootClassName="" sendMessageError={null} /> @@ -2772,7 +2787,10 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` form="OrderDetailsPanel.SendMessageForm" inProgress={false} messagePlaceholder="OrderDetailsPanel.sendMessagePlaceholder" + onBlur={[Function]} + onFocus={[Function]} onSubmit={[Function]} + rootClassName="" sendMessageError={null} /> diff --git a/src/util/userAgent.js b/src/util/userAgent.js new file mode 100644 index 00000000..509da479 --- /dev/null +++ b/src/util/userAgent.js @@ -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; +}; From d2ac3997b228c8bb052ca8a19eb1369cf3d7a074 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Wed, 15 Nov 2017 13:40:10 +0200 Subject: [PATCH 7/7] Fix button spinner stroke width regression --- src/components/Button/Button.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Button/Button.css b/src/components/Button/Button.css index d40633d5..e3ea54a3 100644 --- a/src/components/Button/Button.css +++ b/src/components/Button/Button.css @@ -91,6 +91,7 @@ width: 28px; height: 28px; stroke: var(--matterColorLight); + stroke-width: 3px; } .checkmark {