diff --git a/src/components/ActivityFeed/ActivityFeed.example.js b/src/components/ActivityFeed/ActivityFeed.example.js index 8740b28d..9af2b14a 100644 --- a/src/components/ActivityFeed/ActivityFeed.example.js +++ b/src/components/ActivityFeed/ActivityFeed.example.js @@ -18,6 +18,7 @@ export const Empty = { props: { messages: [], hasOlderMessages: false, + onOpenReviewModal: noop, onShowOlderMessages: noop, fetchMessagesInProgress: false, }, @@ -32,6 +33,7 @@ export const WithoutCurrentUser = { createMessage('msg2', {}, { sender: createUser('user2') }), ], hasOlderMessages: false, + onOpenReviewModal: noop, onShowOlderMessages: noop, fetchMessagesInProgress: false, }, @@ -50,6 +52,7 @@ export const WithCurrentUser = { createMessage('msg5', {}, { sender: createUser('user1') }), ], hasOlderMessages: false, + onOpenReviewModal: noop, onShowOlderMessages: noop, fetchMessagesInProgress: false, }, @@ -79,6 +82,7 @@ export const WithTransitions = { }), messages: [], hasOlderMessages: false, + onOpenReviewModal: noop, onShowOlderMessages: noop, fetchMessagesInProgress: false, }, @@ -162,6 +166,7 @@ export const WithMessagesTransitionsAndReviews = { ), ], hasOlderMessages: false, + onOpenReviewModal: noop, onShowOlderMessages: noop, fetchMessagesInProgress: false, }, @@ -204,6 +209,7 @@ export const WithAReviewFromBothUsers = { }), messages: [], hasOlderMessages: false, + onOpenReviewModal: noop, onShowOlderMessages: noop, fetchMessagesInProgress: false, }, @@ -278,6 +284,7 @@ class PagedFeed extends Component { transaction, messages, hasOlderMessages: !this.state.showAllMessages, + onOpenReviewModal: noop, onShowOlderMessages: handleShowOlder, fetchMessagesInProgress: false, }; diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js index 415f63cb..53b0edce 100644 --- a/src/components/ActivityFeed/ActivityFeed.js +++ b/src/components/ActivityFeed/ActivityFeed.js @@ -76,19 +76,14 @@ const hasUserLeftAReviewFirst = (userRole, lastTransition) => { ); }; -const onLeaveAReview = () => { - // TODO: Open up a review modal - - console.log('Leave a review button clicked'); -}; - const resolveTransitionMessage = ( transition, lastTransition, listingTitle, ownRole, otherUsersName, - intl + intl, + onOpenReviewModal ) => { const isOwnTransition = transition.by === ownRole; const currentTransition = transition.transition; @@ -130,7 +125,7 @@ const resolveTransitionMessage = ( // if current user is not the first to leave a review const reviewLink = deliveredState || !hasUserLeftAReviewFirst(ownRole, lastTransition) ? ( - + ) : null; @@ -144,7 +139,7 @@ const resolveTransitionMessage = ( // show the leave a review link if current user is not the first // one to leave a review const reviewLink = !hasUserLeftAReviewFirst(ownRole, lastTransition) ? ( - + ) : null; @@ -178,7 +173,7 @@ const reviewByAuthorId = (transaction, userId) => { }; const Transition = props => { - const { transition, transaction, currentUser, intl } = props; + const { transition, transaction, currentUser, intl, onOpenReviewModal } = props; const currentTransaction = ensureTransaction(transaction); const customer = currentTransaction.customer; @@ -202,7 +197,8 @@ const Transition = props => { listingTitle, ownRole, otherUsersName, - intl + intl, + onOpenReviewModal ); const currentTransition = transition.transition; @@ -252,6 +248,7 @@ Transition.propTypes = { transaction: propTypes.transaction.isRequired, currentUser: propTypes.currentUser.isRequired, intl: intlShape.isRequired, + onOpenReviewModal: func.isRequired, }; const EmptyTransition = () => { @@ -296,6 +293,7 @@ export const ActivityFeedComponent = props => { transaction, currentUser, hasOlderMessages, + onOpenReviewModal, onShowOlderMessages, fetchMessagesInProgress, intl, @@ -329,6 +327,7 @@ export const ActivityFeedComponent = props => { transaction={transaction} currentUser={currentUser} intl={intl} + onOpenReviewModal={onOpenReviewModal} /> ); } else { @@ -398,6 +397,7 @@ ActivityFeedComponent.propTypes = { transaction: propTypes.transaction, messages: arrayOf(propTypes.message), hasOlderMessages: bool.isRequired, + onOpenReviewModal: func.isRequired, onShowOlderMessages: func.isRequired, fetchMessagesInProgress: bool.isRequired, diff --git a/src/components/ActivityFeed/ActivityFeed.test.js b/src/components/ActivityFeed/ActivityFeed.test.js index 8281e746..9925a07d 100644 --- a/src/components/ActivityFeed/ActivityFeed.test.js +++ b/src/components/ActivityFeed/ActivityFeed.test.js @@ -21,6 +21,7 @@ describe('ActivityFeed', () => { createMessage('msg2', {}, { sender: createUser('user2') }), ], hasOlderMessages: false, + onOpenReviewModal: noop, onShowOlderMessages: noop, fetchMessagesInProgress: false, intl: fakeIntl, diff --git a/src/components/FieldReviewRating/FieldReviewRating.css b/src/components/FieldReviewRating/FieldReviewRating.css new file mode 100644 index 00000000..0161ab21 --- /dev/null +++ b/src/components/FieldReviewRating/FieldReviewRating.css @@ -0,0 +1,52 @@ +@import '../../marketplace.css'; + +.ratingFieldSet { + border: none; + padding: 0; + margin: 0; +} + +/* fieldset can't use flexbox hence this wrapper exists */ +.rating { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + margin-top: 16px; +} + +.rateInput { + display: none; +} + +.label { + width: 30px; + cursor: pointer; +} + +.star { + fill: var(--matterColorNegative); +} + +/***** CSS Magic to Highlight Stars on Hover *****/ + +/* Star order: reverse expected (5 -> 1) and also input before label */ + +/* show actived star when checked */ +/* and actived star when hovering over a star */ +/* and show previous stars also as activated */ +.rating > .rateInput:checked ~ .label > .star, +.rating > .label:hover > .star, +.rating > .label:hover ~ .label > .star { + fill: var(--marketplaceColor); +} + +/* Darken hovered star when changing rating i.e it already is active */ +/* and darken hovered star too when changing rating (hovering inside current selection) */ +/* and darken current selection inside hovered selection */ +/* and darken hovered selection inside current selection */ +.rating > .rateInput:checked + .label:hover > .star, +.rating > .rateInput:checked ~ .label:hover > .star, +.rating > .label:hover ~ .rateInput:checked ~ .label > .star, +.rating > .rateInput:checked ~ .label:hover ~ .label > .star { + fill: var(--marketplaceColorDark); +} diff --git a/src/components/FieldReviewRating/FieldReviewRating.example.js b/src/components/FieldReviewRating/FieldReviewRating.example.js new file mode 100644 index 00000000..53512eb0 --- /dev/null +++ b/src/components/FieldReviewRating/FieldReviewRating.example.js @@ -0,0 +1,44 @@ +/* eslint-disable no-console */ +import React from 'react'; +import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import * as validators from '../../util/validators'; +import { Button } from '../../components'; +import FieldReviewRating from './FieldReviewRating'; + +const formName = 'Styleguide.FieldReviewRating.Form'; + +const FormComponent = props => { + const { form, handleSubmit, invalid, pristine, submitting } = props; + const required = validators.required('This field is required'); + const submitDisabled = invalid || pristine || submitting; + + return ( +
+ + + + ); +}; + +FormComponent.propTypes = formPropTypes; + +const Form = reduxForm({ + form: formName, +})(FormComponent); + +export const StarRating = { + component: Form, + props: { + onSubmit: values => { + console.log('submit values:', values); + }, + }, + group: 'inputs', +}; diff --git a/src/components/FieldReviewRating/FieldReviewRating.js b/src/components/FieldReviewRating/FieldReviewRating.js new file mode 100644 index 00000000..68ef0c6f --- /dev/null +++ b/src/components/FieldReviewRating/FieldReviewRating.js @@ -0,0 +1,140 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { intlShape, injectIntl } from 'react-intl'; +import { Field } from 'redux-form'; +import classNames from 'classnames'; +import { IconReviewStar, ValidationError } from '../../components'; + +import css from './FieldReviewRating.css'; + +class FieldReviewRatingComponent extends Component { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + componentWillUnmount() { + if (this.props.clearOnUnmount) { + this.props.input.onChange(''); + } + } + + handleChange(event) { + this.props.input.onChange(event.target.value); + } + + render() { + /* eslint-disable no-unused-vars */ + const { + rootClassName, + className, + inputRootClass, + clearOnUnmount, + customErrorText, + id, + intl, + label, + input, + meta, + ...rest + } = this.props; + /* eslint-enable no-unused-vars */ + + const { touched, error } = meta; + const errorText = customErrorText || error; + const fieldMeta = { touched, error: errorText }; + + const { value, ...restInputProps } = input; + const inputProps = { ...restInputProps, type: 'radio', name: 'rating', ...rest }; + + const classes = classNames(rootClassName || css.root, className); + + const createStarRating = starCount => { + let inputsAndLabels = []; + + // Star inpu order: reverse order expected (5 -> 1) and also input before label + // This is due to CSS selectors. + // Sibling combinator (~) selects following siblings, but we want to select previous siblings + for (let i = starCount; i > 0; i--) { + const inputValue = `${i}`; + const starId = `star${i}`; + const inputId = `${id}.${starId}`; + + inputsAndLabels.push( + + ); + + inputsAndLabels.push( + + ); + } + return inputsAndLabels; + }; + + return ( +
+
{ + this.ratingFieldSet = c; + }} + > + {label ? {label} : null} +
{createStarRating(5)}
+
+ +
+ ); + } +} + +FieldReviewRatingComponent.defaultProps = { + rootClassName: null, + className: null, + clearOnUnmount: false, + customErrorText: null, + label: null, +}; + +const { string, bool, shape, func, object } = PropTypes; + +FieldReviewRatingComponent.propTypes = { + rootClassName: string, + className: string, + clearOnUnmount: bool, + id: string.isRequired, + label: string, + + // Error message that can be manually passed to input field, + // overrides default validation message + customErrorText: string, + + // Generated by redux-form's Field component + input: shape({ + onChange: func.isRequired, + }).isRequired, + meta: object.isRequired, + + // from injectIntl + intl: intlShape.isRequired, +}; + +const FieldReviewRating = props => { + return ; +}; + +export default injectIntl(FieldReviewRating); diff --git a/src/components/IconReviewUser/IconReviewUser.css b/src/components/IconReviewUser/IconReviewUser.css new file mode 100644 index 00000000..7480319c --- /dev/null +++ b/src/components/IconReviewUser/IconReviewUser.css @@ -0,0 +1,5 @@ +@import '../../marketplace.css'; + +.root { + stroke: var(--marketplaceColor); +} diff --git a/src/components/IconReviewUser/IconReviewUser.example.js b/src/components/IconReviewUser/IconReviewUser.example.js new file mode 100644 index 00000000..5b47352e --- /dev/null +++ b/src/components/IconReviewUser/IconReviewUser.example.js @@ -0,0 +1,7 @@ +import IconReviewUser from './IconReviewUser'; + +export const Icon = { + component: IconReviewUser, + props: {}, + group: 'icons', +}; diff --git a/src/components/IconReviewUser/IconReviewUser.js b/src/components/IconReviewUser/IconReviewUser.js new file mode 100644 index 00000000..bc00c190 --- /dev/null +++ b/src/components/IconReviewUser/IconReviewUser.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import css from './IconReviewUser.css'; + +const IconReviewUser = props => { + const { className, rootClassName } = props; + const classes = classNames(rootClassName || css.root, className); + + return ( + + + + + + + + + ); +}; + +IconReviewUser.defaultProps = { className: null, rootClassName: null }; + +const { string } = PropTypes; + +IconReviewUser.propTypes = { + className: string, + rootClassName: string, +}; + +export default IconReviewUser; diff --git a/src/components/Modal/Modal.css b/src/components/Modal/Modal.css index 74e62042..45d6a9be 100644 --- a/src/components/Modal/Modal.css +++ b/src/components/Modal/Modal.css @@ -44,6 +44,9 @@ } } +.content { +} + /* Content is explicitly hidden (this default can be overridden with passed-in class) */ /* The use case for having both .isOpen and .isClosed is ModalInMobile use case */ /* where desktop layout should not get any styling from Modal component. */ diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js index 6d5ce15b..f60a843c 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.js @@ -7,11 +7,12 @@ import { createSlug } from '../../util/urlHelpers'; import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data'; import { isMobileSafari } from '../../util/userAgent'; import { + ActivityFeed, + AvatarMedium, BookingBreakdown, NamedLink, ResponsiveImage, - AvatarMedium, - ActivityFeed, + ReviewModal, } from '../../components'; import { SendMessageForm } from '../../containers'; @@ -82,8 +83,31 @@ const orderMessage = (transaction, providerName) => { export class OrderDetailsPanelComponent extends Component { constructor(props) { super(props); - this.state = { sendMessageFormFocused: false }; + this.state = { + sendMessageFormFocused: false, + isReviewModalOpen: false, + reviewSubmitted: false, + }; + this.onOpenReviewModal = this.onOpenReviewModal.bind(this); + this.onSubmitReview = this.onSubmitReview.bind(this); } + + onOpenReviewModal() { + this.setState({ isReviewModalOpen: true }); + } + + onSubmitReview(values) { + const { onSendReview, transaction } = this.props; + const currentTransaction = ensureTransaction(transaction); + const { reviewRating, reviewContent } = values; + const rating = Number.parseInt(reviewRating, 10); + onSendReview(currentTransaction, rating, reviewContent) + .then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true })) + .catch(e => { + // Do nothing. + }); + } + render() { const { rootClassName, @@ -97,6 +121,9 @@ export class OrderDetailsPanelComponent extends Component { fetchMessagesError, sendMessageInProgress, sendMessageError, + sendReviewInProgress, + sendReviewError, + onManageDisableScrolling, onShowMoreMessages, onSendMessage, onResetForm, @@ -200,6 +227,7 @@ export class OrderDetailsPanelComponent extends Component { transaction={currentTransaction} currentUser={currentUser} hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress} + onOpenReviewModal={this.onOpenReviewModal} onShowOlderMessages={handleShowOlderMessages} fetchMessagesInProgress={fetchMessagesInProgress} /> @@ -330,6 +358,17 @@ export class OrderDetailsPanelComponent extends Component { + this.setState({ isReviewModalOpen: false })} + onManageDisableScrolling={onManageDisableScrolling} + onSubmitReview={this.onSubmitReview} + revieweeName={authorDisplayName} + reviewSent={this.state.reviewSubmitted} + sendReviewInProgress={sendReviewInProgress} + sendReviewError={sendReviewError} + /> ); } @@ -341,6 +380,7 @@ OrderDetailsPanelComponent.defaultProps = { currentUser: null, fetchMessagesError: null, sendMessageError: null, + sendReviewError: null, }; const { string, arrayOf, bool, func, number } = PropTypes; @@ -358,8 +398,12 @@ OrderDetailsPanelComponent.propTypes = { fetchMessagesError: propTypes.error, sendMessageInProgress: bool.isRequired, sendMessageError: propTypes.error, + sendReviewInProgress: bool.isRequired, + sendReviewError: propTypes.error, + onManageDisableScrolling: func.isRequired, onShowMoreMessages: func.isRequired, onSendMessage: func.isRequired, + onSendReview: func.isRequired, onResetForm: func.isRequired, // from injectIntl diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js index bdb66b16..df68e3c5 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js @@ -78,8 +78,12 @@ describe('OrderDetailsPanel', () => { initialMessageFailed: false, fetchMessagesInProgress: false, sendMessageInProgress: false, + sendReviewInProgress: false, + onManageDisableScrolling: noop, + onOpenReviewModal: noop, onShowMoreMessages: noop, onSendMessage: noop, + onSendReview: noop, onResetForm: noop, }; diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap index 71cf60de..241719cc 100644 --- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap +++ b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap @@ -310,6 +310,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -644,6 +645,17 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = ` + `; @@ -945,6 +957,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -1279,6 +1292,17 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = ` + `; @@ -1592,6 +1616,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -1926,6 +1951,17 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = ` + `; @@ -2227,6 +2263,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -2561,6 +2598,17 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = ` + `; @@ -2862,6 +2910,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -3196,6 +3245,17 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = ` + `; @@ -3520,6 +3580,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -3854,5 +3915,16 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = ` + `; diff --git a/src/components/ReviewModal/ReviewModal.css b/src/components/ReviewModal/ReviewModal.css new file mode 100644 index 00000000..4b6ceafc --- /dev/null +++ b/src/components/ReviewModal/ReviewModal.css @@ -0,0 +1,33 @@ +@import '../../marketplace.css'; + +.root { + @apply --marketplaceModalBaseStyles; + padding-top: 70px; + + @media (--viewportMedium) { + flex-basis: 567px; + } +} + +.modalContent { + flex-grow: 1; +} + +/* Icon of the modal */ +.modalIcon { + @apply --marketplaceModalIconStyles; +} + +/* Title of the modal */ +.modalTitle { + @apply --marketplaceModalTitleStyles; +} + +.reviewee { + white-space: nowrap; +} + +/* Paragraph for the Modal */ +.modalMessage { + @apply --marketplaceModalParagraphStyles; +} diff --git a/src/components/ReviewModal/ReviewModal.js b/src/components/ReviewModal/ReviewModal.js new file mode 100644 index 00000000..91196e11 --- /dev/null +++ b/src/components/ReviewModal/ReviewModal.js @@ -0,0 +1,77 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, intlShape, injectIntl } from 'react-intl'; +import classNames from 'classnames'; +import * as propTypes from '../../util/propTypes'; +import { IconReviewUser, Modal } from '../../components'; +import { ReviewForm } from '../../containers'; + +import css from './ReviewModal.css'; + +const ReviewModal = props => { + const { + className, + rootClassName, + id, + intl, + isOpen, + onCloseModal, + onManageDisableScrolling, + onSubmitReview, + revieweeName, + reviewSent, + sendReviewInProgress, + sendReviewError, + } = props; + + const classes = classNames(rootClassName || css.root, className); + const closeButtonMessage = intl.formatMessage({ id: 'ReviewModal.later' }); + const reviewee = {revieweeName}; + + return ( + + +

+ +

+

+ +

+ +
+ ); +}; + +const { bool, string } = PropTypes; + +ReviewModal.defaultProps = { + className: null, + rootClassName: null, + reviewSent: false, + sendReviewInProgress: false, + sendReviewError: null, +}; + +ReviewModal.propTypes = { + className: string, + rootClassName: string, + intl: intlShape.isRequired, + reviewSent: bool, + sendReviewInProgress: bool, + sendReviewError: propTypes.error, +}; + +export default injectIntl(ReviewModal); diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.js index 8b29437b..2676d0f5 100644 --- a/src/components/SaleDetailsPanel/SaleDetailsPanel.js +++ b/src/components/SaleDetailsPanel/SaleDetailsPanel.js @@ -7,14 +7,15 @@ import { createSlug } from '../../util/urlHelpers'; import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data'; import { isMobileSafari } from '../../util/userAgent'; import { + ActivityFeed, AvatarLarge, AvatarMedium, BookingBreakdown, NamedLink, - ResponsiveImage, PrimaryButton, + ResponsiveImage, + ReviewModal, SecondaryButton, - ActivityFeed, } from '../../components'; import { SendMessageForm } from '../../containers'; @@ -91,8 +92,31 @@ const saleInfoText = (transaction, customerName) => { export class SaleDetailsPanelComponent extends Component { constructor(props) { super(props); - this.state = { sendMessageFormFocused: false }; + this.state = { + sendMessageFormFocused: false, + isReviewModalOpen: false, + reviewSubmitted: false, + }; + this.onOpenReviewModal = this.onOpenReviewModal.bind(this); + this.onSubmitReview = this.onSubmitReview.bind(this); } + + onOpenReviewModal() { + this.setState({ isReviewModalOpen: true }); + } + + onSubmitReview(values) { + const { onSendReview, transaction } = this.props; + const currentTransaction = ensureTransaction(transaction); + const { reviewRating, reviewContent } = values; + const rating = Number.parseInt(reviewRating, 10); + onSendReview(currentTransaction, rating, reviewContent) + .then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true })) + .catch(e => { + // Do nothing. + }); + } + render() { const { rootClassName, @@ -111,6 +135,9 @@ export class SaleDetailsPanelComponent extends Component { messages, sendMessageInProgress, sendMessageError, + sendReviewInProgress, + sendReviewError, + onManageDisableScrolling, onShowMoreMessages, onSendMessage, onResetForm, @@ -196,6 +223,7 @@ export class SaleDetailsPanelComponent extends Component { transaction={currentTransaction} currentUser={currentUser} hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress} + onOpenReviewModal={this.onOpenReviewModal} onShowOlderMessages={handleShowOlderMessages} fetchMessagesInProgress={fetchMessagesInProgress} /> @@ -361,6 +389,17 @@ export class SaleDetailsPanelComponent extends Component { + this.setState({ isReviewModalOpen: false })} + onManageDisableScrolling={onManageDisableScrolling} + onSubmitReview={this.onSubmitReview} + revieweeName={customerDisplayName} + reviewSent={this.state.reviewSubmitted} + sendReviewInProgress={sendReviewInProgress} + sendReviewError={sendReviewError} + /> ); } @@ -374,6 +413,7 @@ SaleDetailsPanelComponent.defaultProps = { declineSaleError: null, fetchMessagesError: null, sendMessageError: null, + sendReviewError: null, }; const { bool, func, string, arrayOf, number } = PropTypes; @@ -396,8 +436,12 @@ SaleDetailsPanelComponent.propTypes = { messages: arrayOf(propTypes.message).isRequired, sendMessageInProgress: bool.isRequired, sendMessageError: propTypes.error, + sendReviewInProgress: bool.isRequired, + sendReviewError: propTypes.error, + onManageDisableScrolling: func.isRequired, onShowMoreMessages: func.isRequired, onSendMessage: func.isRequired, + onSendReview: func.isRequired, onResetForm: func.isRequired, // from injectIntl diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js index b5a99760..1f1fc568 100644 --- a/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js +++ b/src/components/SaleDetailsPanel/SaleDetailsPanel.test.js @@ -80,8 +80,12 @@ describe('SaleDetailsPanel', () => { ], fetchMessagesInProgress: false, sendMessageInProgress: false, + sendReviewInProgress: false, + onManageDisableScrolling: noop, + onOpenReviewModal: noop, onShowMoreMessages: noop, onSendMessage: noop, + onSendReview: noop, onResetForm: noop, intl: fakeIntl, }; diff --git a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap index 020ddbac..f906332e 100644 --- a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap +++ b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap @@ -304,6 +304,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -584,6 +585,17 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = ` + `; @@ -891,6 +903,7 @@ exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -1171,6 +1184,17 @@ exports[`SaleDetailsPanel autodeclined matches snapshot 1`] = ` + `; @@ -1478,6 +1502,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -1758,6 +1783,17 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = ` + `; @@ -2065,6 +2101,7 @@ exports[`SaleDetailsPanel declined matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -2345,6 +2382,17 @@ exports[`SaleDetailsPanel declined matches snapshot 1`] = ` + `; @@ -2652,6 +2700,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -2932,6 +2981,17 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = ` + `; @@ -3249,6 +3309,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` }, ] } + onOpenReviewModal={[Function]} onShowOlderMessages={[Function]} transaction={ Object { @@ -3585,5 +3646,16 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` + `; diff --git a/src/components/TextInputField/TextInputField.js b/src/components/TextInputField/TextInputField.js index 45aeeb84..4e8f0ed2 100644 --- a/src/components/TextInputField/TextInputField.js +++ b/src/components/TextInputField/TextInputField.js @@ -6,6 +6,8 @@ import { ValidationError, ExpandingTextarea } from '../../components'; import css from './TextInputField.css'; +const CONTENT_MAX_LENGTH = 5000; + class TextInputFieldComponent extends Component { componentWillUnmount() { if (this.props.clearOnUnmount) { @@ -52,7 +54,7 @@ class TextInputFieldComponent extends Component { [css.textarea]: isTextarea, }); const inputProps = isTextarea - ? { className: inputClasses, id, ...input, ...rest } + ? { className: inputClasses, id, rows: 1, maxLength: CONTENT_MAX_LENGTH, ...input, ...rest } : { className: inputClasses, id, type, ...input, ...rest }; const classes = classNames(rootClassName || css.root, className); diff --git a/src/components/index.js b/src/components/index.js index 22c61959..e23e9ad5 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -27,6 +27,7 @@ export { default as EditListingWizard } from './EditListingWizard/EditListingWiz export { default as ExpandingTextarea } from './ExpandingTextarea/ExpandingTextarea'; export { default as ExternalLink } from './ExternalLink/ExternalLink'; export { default as FilterPanel } from './FilterPanel/FilterPanel'; +export { default as FieldReviewRating } from './FieldReviewRating/FieldReviewRating'; export { default as Footer } from './Footer/Footer'; export { default as Form } from './Form/Form'; export { default as IconBannedUser } from './IconBannedUser/IconBannedUser'; @@ -38,6 +39,7 @@ export { default as IconEmailSuccess } from './IconEmailSuccess/IconEmailSuccess export { default as IconKeys } from './IconKeys/IconKeys'; export { default as IconKeysSuccess } from './IconKeysSuccess/IconKeysSuccess'; export { default as IconReviewStar } from './IconReviewStar/IconReviewStar'; +export { default as IconReviewUser } from './IconReviewUser/IconReviewUser'; export { default as IconSearch } from './IconSearch/IconSearch'; export { default as IconSocialMediaFacebook, @@ -81,6 +83,7 @@ export { default as PrivacyPolicy } from './PrivacyPolicy/PrivacyPolicy'; export { default as Promised } from './Promised/Promised'; export { default as ResponsiveImage } from './ResponsiveImage/ResponsiveImage'; export { default as ReviewRating } from './ReviewRating/ReviewRating'; +export { default as ReviewModal } from './ReviewModal/ReviewModal'; export { default as SaleDetailsPanel } from './SaleDetailsPanel/SaleDetailsPanel'; export { default as SearchMap } from './SearchMap/SearchMap'; export { default as SearchMapGroupLabel } from './SearchMapGroupLabel/SearchMapGroupLabel'; diff --git a/src/containers/EditListingDescriptionForm/__snapshots__/EditListingDescriptionForm.test.js.snap b/src/containers/EditListingDescriptionForm/__snapshots__/EditListingDescriptionForm.test.js.snap index 4145669d..3859ca0a 100644 --- a/src/containers/EditListingDescriptionForm/__snapshots__/EditListingDescriptionForm.test.js.snap +++ b/src/containers/EditListingDescriptionForm/__snapshots__/EditListingDescriptionForm.test.js.snap @@ -42,6 +42,7 @@ exports[`EditListingDescriptionForm matches snapshot 1`] = `