From a7b7c58d80734f30f78a9cbdc2d73f12292a3fd8 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 10 Jan 2019 17:51:13 +0200 Subject: [PATCH 01/12] Extract transaction related code to its own util file from types.js --- .../ActivityFeed/ActivityFeed.example.js | 2 +- src/components/ActivityFeed/ActivityFeed.js | 4 +- .../BookingBreakdown.example.js | 6 +- .../BookingBreakdown/BookingBreakdown.test.js | 4 +- .../BookingBreakdown/LineItemTotalPrice.js | 3 +- .../TransactionPanel/TransactionPanel.test.js | 4 +- .../TransactionPanel.test.js.snap | 10290 +--------------- .../CheckoutPage/CheckoutPage.duck.js | 2 +- .../CheckoutPageSessionHelpers.js | 2 +- src/containers/InboxPage/InboxPage.js | 6 +- src/containers/InboxPage/InboxPage.test.js | 3 +- .../ListingPage/ListingPage.duck.js | 2 +- .../TransactionPage/TransactionPage.duck.js | 2 +- .../TransactionPage/TransactionPage.test.js | 2 +- src/ducks/user.duck.js | 7 +- .../BookingDatesForm/BookingDatesForm.test.js | 3 +- .../EstimatedBreakdownMaybe.js | 9 +- src/util/test-data.js | 5 +- src/util/transaction.js | 119 + src/util/types.js | 121 +- 20 files changed, 596 insertions(+), 10000 deletions(-) create mode 100644 src/util/transaction.js diff --git a/src/components/ActivityFeed/ActivityFeed.example.js b/src/components/ActivityFeed/ActivityFeed.example.js index 8d94c2e2..af9dbe80 100644 --- a/src/components/ActivityFeed/ActivityFeed.example.js +++ b/src/components/ActivityFeed/ActivityFeed.example.js @@ -20,7 +20,7 @@ import { TRANSITION_REVIEW_2_BY_PROVIDER, TX_TRANSITION_ACTOR_CUSTOMER, TX_TRANSITION_ACTOR_PROVIDER, -} from '../../util/types'; +} from '../../util/transaction'; import ActivityFeed from './ActivityFeed'; const noop = () => null; diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js index aec9f100..e92f7dad 100644 --- a/src/components/ActivityFeed/ActivityFeed.js +++ b/src/components/ActivityFeed/ActivityFeed.js @@ -21,8 +21,8 @@ import { TX_TRANSITION_ACTOR_CUSTOMER, TX_TRANSITION_ACTOR_PROVIDER, areReviewsCompleted, - propTypes, -} from '../../util/types'; +} from '../../util/transaction'; +import { propTypes } from '../../util/types'; import * as log from '../../util/log'; import css from './ActivityFeed.css'; diff --git a/src/components/BookingBreakdown/BookingBreakdown.example.js b/src/components/BookingBreakdown/BookingBreakdown.example.js index ef262952..a326183b 100644 --- a/src/components/BookingBreakdown/BookingBreakdown.example.js +++ b/src/components/BookingBreakdown/BookingBreakdown.example.js @@ -1,9 +1,6 @@ import Decimal from 'decimal.js'; import { types as sdkTypes } from '../../util/sdkLoader'; import { - LINE_ITEM_DAY, - LINE_ITEM_NIGHT, - LINE_ITEM_UNITS, TRANSITION_ACCEPT, TRANSITION_CANCEL, TRANSITION_COMPLETE, @@ -11,7 +8,8 @@ import { TRANSITION_EXPIRE, TRANSITION_REQUEST, TX_TRANSITION_ACTOR_CUSTOMER, -} from '../../util/types'; +} from '../../util/transaction'; +import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types'; import config from '../../config'; import BookingBreakdown from './BookingBreakdown'; diff --git a/src/components/BookingBreakdown/BookingBreakdown.test.js b/src/components/BookingBreakdown/BookingBreakdown.test.js index b4263d54..f6736293 100644 --- a/src/components/BookingBreakdown/BookingBreakdown.test.js +++ b/src/components/BookingBreakdown/BookingBreakdown.test.js @@ -4,11 +4,11 @@ import { fakeIntl, createBooking } from '../../util/test-data'; import { renderDeep } from '../../util/test-helpers'; import { types as sdkTypes } from '../../util/sdkLoader'; import { - LINE_ITEM_NIGHT, TRANSITION_CANCEL, TRANSITION_REQUEST, TX_TRANSITION_ACTOR_CUSTOMER, -} from '../../util/types'; +} from '../../util/transaction'; +import { LINE_ITEM_NIGHT } from '../../util/types'; import { BookingBreakdownComponent } from './BookingBreakdown'; const { UUID, Money } = sdkTypes; diff --git a/src/components/BookingBreakdown/LineItemTotalPrice.js b/src/components/BookingBreakdown/LineItemTotalPrice.js index 40c4b98b..92099050 100644 --- a/src/components/BookingBreakdown/LineItemTotalPrice.js +++ b/src/components/BookingBreakdown/LineItemTotalPrice.js @@ -2,7 +2,8 @@ import React from 'react'; import { bool } from 'prop-types'; import { FormattedMessage, intlShape } from 'react-intl'; import { formatMoney } from '../../util/currency'; -import { txIsCanceled, txIsCompleted, txIsDeclinedOrExpired, propTypes } from '../../util/types'; +import { txIsCanceled, txIsCompleted, txIsDeclinedOrExpired } from '../../util/transaction'; +import { propTypes } from '../../util/types'; import css from './BookingBreakdown.css'; diff --git a/src/components/TransactionPanel/TransactionPanel.test.js b/src/components/TransactionPanel/TransactionPanel.test.js index b3443d0a..58e83941 100644 --- a/src/components/TransactionPanel/TransactionPanel.test.js +++ b/src/components/TransactionPanel/TransactionPanel.test.js @@ -19,8 +19,8 @@ import { TRANSITION_ENQUIRE, TRANSITION_EXPIRE, TRANSITION_REQUEST, -} from '../../util/types'; -import { BreakdownMaybe } from './TransactionPanel.helpers'; +} from '../../util/transaction'; +import BreakdownMaybe from './BreakdownMaybe'; import { TransactionPanelComponent } from './TransactionPanel'; const noop = () => null; diff --git a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap index cac213d5..4a0ea83d 100644 --- a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap +++ b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap @@ -44,445 +44,38 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = ` } /> - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - null; diff --git a/src/containers/ListingPage/ListingPage.duck.js b/src/containers/ListingPage/ListingPage.duck.js index c64ee5f4..54c83262 100644 --- a/src/containers/ListingPage/ListingPage.duck.js +++ b/src/containers/ListingPage/ListingPage.duck.js @@ -5,7 +5,7 @@ import { types as sdkTypes } from '../../util/sdkLoader'; import { storableError } from '../../util/errors'; import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck'; import { denormalisedResponseEntities } from '../../util/data'; -import { TRANSITION_ENQUIRE } from '../../util/types'; +import { TRANSITION_ENQUIRE } from '../../util/transaction'; import { LISTING_PAGE_DRAFT_VARIANT, LISTING_PAGE_PENDING_APPROVAL_VARIANT, diff --git a/src/containers/TransactionPage/TransactionPage.duck.js b/src/containers/TransactionPage/TransactionPage.duck.js index f102e14f..59cc018e 100644 --- a/src/containers/TransactionPage/TransactionPage.duck.js +++ b/src/containers/TransactionPage/TransactionPage.duck.js @@ -13,7 +13,7 @@ import { TRANSITION_REVIEW_1_BY_PROVIDER, TRANSITION_REVIEW_2_BY_CUSTOMER, TRANSITION_REVIEW_2_BY_PROVIDER, -} from '../../util/types'; +} from '../../util/transaction'; import * as log from '../../util/log'; import { updatedEntities, diff --git a/src/containers/TransactionPage/TransactionPage.test.js b/src/containers/TransactionPage/TransactionPage.test.js index ccec9181..c8a7cd8e 100644 --- a/src/containers/TransactionPage/TransactionPage.test.js +++ b/src/containers/TransactionPage/TransactionPage.test.js @@ -8,7 +8,7 @@ import { fakeIntl, } from '../../util/test-data'; import { renderShallow } from '../../util/test-helpers'; -import { TRANSITION_REQUEST } from '../../util/types'; +import { TRANSITION_REQUEST } from '../../util/transaction'; import { TransactionPageComponent } from './TransactionPage'; const noop = () => null; diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index 0f40fbd2..be852606 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -3,11 +3,8 @@ import isUndefined from 'lodash/isUndefined'; import config from '../config'; import { denormalisedResponseEntities, ensureOwnListing } from '../util/data'; import { storableError } from '../util/errors'; -import { - LISTING_STATE_DRAFT, - TRANSITION_REQUEST, - TRANSITION_REQUEST_AFTER_ENQUIRY, -} from '../util/types'; +import { TRANSITION_REQUEST, TRANSITION_REQUEST_AFTER_ENQUIRY } from '../util/transaction'; +import { LISTING_STATE_DRAFT } from '../util/types'; import * as log from '../util/log'; import { authInfo } from './Auth.duck'; diff --git a/src/forms/BookingDatesForm/BookingDatesForm.test.js b/src/forms/BookingDatesForm/BookingDatesForm.test.js index 0ff5fbf4..31108a4f 100644 --- a/src/forms/BookingDatesForm/BookingDatesForm.test.js +++ b/src/forms/BookingDatesForm/BookingDatesForm.test.js @@ -4,7 +4,8 @@ import Decimal from 'decimal.js'; import { types as sdkTypes } from '../../util/sdkLoader'; import { renderShallow, renderDeep } from '../../util/test-helpers'; import { fakeIntl } from '../../util/test-data'; -import { LINE_ITEM_NIGHT, TRANSITION_REQUEST } from '../../util/types'; +import { TRANSITION_REQUEST } from '../../util/transaction'; +import { LINE_ITEM_NIGHT } from '../../util/types'; import { dateFromAPIToLocalNoon } from '../../util/dates'; import { BookingBreakdown } from '../../components'; import { BookingDatesFormComponent } from './BookingDatesForm'; diff --git a/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js b/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js index c4507e2f..8289d423 100644 --- a/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js +++ b/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js @@ -30,13 +30,8 @@ import moment from 'moment'; import Decimal from 'decimal.js'; import { types as sdkTypes } from '../../util/sdkLoader'; import { dateFromLocalToAPI, nightsBetween, daysBetween } from '../../util/dates'; -import { - LINE_ITEM_DAY, - LINE_ITEM_NIGHT, - LINE_ITEM_UNITS, - TRANSITION_REQUEST, - TX_TRANSITION_ACTOR_CUSTOMER, -} from '../../util/types'; +import { TRANSITION_REQUEST, TX_TRANSITION_ACTOR_CUSTOMER } from '../../util/transaction'; +import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types'; import { unitDivisor, convertMoneyToNumber, convertUnitToSubUnit } from '../../util/currency'; import { BookingBreakdown } from '../../components'; diff --git a/src/util/test-data.js b/src/util/test-data.js index b47347ee..e6f20f41 100644 --- a/src/util/test-data.js +++ b/src/util/test-data.js @@ -7,9 +7,8 @@ import { TRANSITION_REQUEST, TX_TRANSITION_ACTOR_CUSTOMER, TX_TRANSITION_ACTOR_PROVIDER, - LISTING_STATE_PUBLISHED, - TIME_SLOT_DAY, -} from '../util/types'; +} from '../util/transaction'; +import { LISTING_STATE_PUBLISHED, TIME_SLOT_DAY } from '../util/types'; const { UUID, LatLng, Money } = sdkTypes; diff --git a/src/util/transaction.js b/src/util/transaction.js new file mode 100644 index 00000000..cf0c693c --- /dev/null +++ b/src/util/transaction.js @@ -0,0 +1,119 @@ +import { ensureTransaction } from './data'; + +// When a customer makes a booking to a listing, a transaction is +// created with the initial request transition. +export const TRANSITION_REQUEST = 'transition/request'; + +// A customer can also initiate a transaction with an enquiry, and +// then transition that with a request. +export const TRANSITION_ENQUIRE = 'transition/enquire'; +export const TRANSITION_REQUEST_AFTER_ENQUIRY = 'transition/request-after-enquiry'; + +// When the provider accepts or declines a transaction from the +// SalePage, it is transitioned with the accept or decline transition. +export const TRANSITION_ACCEPT = 'transition/accept'; +export const TRANSITION_DECLINE = 'transition/decline'; + +// The backend automatically expire the transaction. +export const TRANSITION_EXPIRE = 'transition/expire'; + +// Admin can also cancel the transition. +export const TRANSITION_CANCEL = 'transition/cancel'; + +// The backend will mark the transaction completed. +export const TRANSITION_COMPLETE = 'transition/complete'; + +// Reviews are given through transaction transitions. Review 1 can be +// by provider or customer, and review 2 will be the other party of +// the transaction. +export const TRANSITION_REVIEW_1_BY_PROVIDER = 'transition/review-1-by-provider'; +export const TRANSITION_REVIEW_2_BY_PROVIDER = 'transition/review-2-by-provider'; +export const TRANSITION_REVIEW_1_BY_CUSTOMER = 'transition/review-1-by-customer'; +export const TRANSITION_REVIEW_2_BY_CUSTOMER = 'transition/review-2-by-customer'; +export const TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD = 'transition/expire-customer-review-period'; +export const TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD = 'transition/expire-provider-review-period'; +export const TRANSITION_EXPIRE_REVIEW_PERIOD = 'transition/expire-review-period'; + +export const TRANSITIONS = [ + TRANSITION_ACCEPT, + TRANSITION_CANCEL, + TRANSITION_COMPLETE, + TRANSITION_DECLINE, + TRANSITION_ENQUIRE, + TRANSITION_EXPIRE, + TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, + TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, + TRANSITION_EXPIRE_REVIEW_PERIOD, + TRANSITION_REQUEST, + TRANSITION_REQUEST_AFTER_ENQUIRY, + TRANSITION_REVIEW_1_BY_CUSTOMER, + TRANSITION_REVIEW_1_BY_PROVIDER, + TRANSITION_REVIEW_2_BY_CUSTOMER, + TRANSITION_REVIEW_2_BY_PROVIDER, +]; + +const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition; + +export const txIsEnquired = tx => txLastTransition(tx) === TRANSITION_ENQUIRE; + +export const txIsRequested = tx => { + const transition = txLastTransition(tx); + return transition === TRANSITION_REQUEST || transition === TRANSITION_REQUEST_AFTER_ENQUIRY; +}; + +export const txIsAccepted = tx => txLastTransition(tx) === TRANSITION_ACCEPT; + +export const txIsDeclined = tx => txLastTransition(tx) === TRANSITION_DECLINE; + +export const txIsExpired = tx => txLastTransition(tx) === TRANSITION_EXPIRE; + +export const txIsDeclinedOrExpired = tx => txIsDeclined(tx) || txIsExpired(tx); + +export const txIsCanceled = tx => txLastTransition(tx) === TRANSITION_CANCEL; + +export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE; + +const firstReviewTransitions = [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_1_BY_PROVIDER]; +export const txHasFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx)); + +export const txIsReviewed = tx => areReviewsCompleted(txLastTransition(tx)); + +// Check if tx transition is followed by a state where +// reviews are completed +export const areReviewsCompleted = transition => { + return [ + TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, + TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, + TRANSITION_EXPIRE_REVIEW_PERIOD, + TRANSITION_REVIEW_2_BY_CUSTOMER, + TRANSITION_REVIEW_2_BY_PROVIDER, + ].includes(transition); +}; + +export const txHasBeenAccepted = tx => { + const transition = txLastTransition(tx); + return [ + TRANSITION_ACCEPT, + TRANSITION_REVIEW_1_BY_CUSTOMER, + TRANSITION_REVIEW_1_BY_PROVIDER, + TRANSITION_REVIEW_2_BY_CUSTOMER, + TRANSITION_REVIEW_2_BY_PROVIDER, + TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, + TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, + TRANSITION_EXPIRE_REVIEW_PERIOD, + TRANSITION_COMPLETE, + ].includes(transition); +}; + +// Roles of actors that perform transaction transitions +export const TX_TRANSITION_ACTOR_CUSTOMER = 'customer'; +export const TX_TRANSITION_ACTOR_PROVIDER = 'provider'; +export const TX_TRANSITION_ACTOR_SYSTEM = 'system'; +export const TX_TRANSITION_ACTOR_OPERATOR = 'operator'; + +export const TX_TRANSITION_ACTORS = [ + TX_TRANSITION_ACTOR_CUSTOMER, + TX_TRANSITION_ACTOR_PROVIDER, + TX_TRANSITION_ACTOR_SYSTEM, + TX_TRANSITION_ACTOR_OPERATOR, +]; diff --git a/src/util/types.js b/src/util/types.js index 88932196..5ab9070e 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -32,7 +32,7 @@ import { } from 'prop-types'; import Decimal from 'decimal.js'; import { types as sdkTypes } from './sdkLoader'; -import { ensureTransaction } from './data'; +import { TRANSITIONS, TX_TRANSITION_ACTORS } from './transaction'; const { UUID, LatLng, LatLngBounds, Money } = sdkTypes; @@ -231,131 +231,12 @@ propTypes.availabilityException = shape({ }), }); -// When a customer makes a booking to a listing, a transaction is -// created with the initial request transition. -export const TRANSITION_REQUEST = 'transition/request'; - -// A customer can also initiate a transaction with an enquiry, and -// then transition that with a request. -export const TRANSITION_ENQUIRE = 'transition/enquire'; -export const TRANSITION_REQUEST_AFTER_ENQUIRY = 'transition/request-after-enquiry'; - -// When the provider accepts or declines a transaction from the -// SalePage, it is transitioned with the accept or decline transition. -export const TRANSITION_ACCEPT = 'transition/accept'; -export const TRANSITION_DECLINE = 'transition/decline'; - -// The backend automatically expire the transaction. -export const TRANSITION_EXPIRE = 'transition/expire'; - -// Admin can also cancel the transition. -export const TRANSITION_CANCEL = 'transition/cancel'; - -// The backend will mark the transaction completed. -export const TRANSITION_COMPLETE = 'transition/complete'; - -// Reviews are given through transaction transitions. Review 1 can be -// by provider or customer, and review 2 will be the other party of -// the transaction. -export const TRANSITION_REVIEW_1_BY_PROVIDER = 'transition/review-1-by-provider'; -export const TRANSITION_REVIEW_2_BY_PROVIDER = 'transition/review-2-by-provider'; -export const TRANSITION_REVIEW_1_BY_CUSTOMER = 'transition/review-1-by-customer'; -export const TRANSITION_REVIEW_2_BY_CUSTOMER = 'transition/review-2-by-customer'; -export const TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD = 'transition/expire-customer-review-period'; -export const TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD = 'transition/expire-provider-review-period'; -export const TRANSITION_EXPIRE_REVIEW_PERIOD = 'transition/expire-review-period'; - -export const TRANSITIONS = [ - TRANSITION_ACCEPT, - TRANSITION_CANCEL, - TRANSITION_COMPLETE, - TRANSITION_DECLINE, - TRANSITION_ENQUIRE, - TRANSITION_EXPIRE, - TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, - TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, - TRANSITION_EXPIRE_REVIEW_PERIOD, - TRANSITION_REQUEST, - TRANSITION_REQUEST_AFTER_ENQUIRY, - TRANSITION_REVIEW_1_BY_CUSTOMER, - TRANSITION_REVIEW_1_BY_PROVIDER, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, -]; - -// Roles of actors that perform transaction transitions -export const TX_TRANSITION_ACTOR_CUSTOMER = 'customer'; -export const TX_TRANSITION_ACTOR_PROVIDER = 'provider'; -export const TX_TRANSITION_ACTOR_SYSTEM = 'system'; -export const TX_TRANSITION_ACTOR_OPERATOR = 'operator'; - -export const TX_TRANSITION_ACTORS = [ - TX_TRANSITION_ACTOR_CUSTOMER, - TX_TRANSITION_ACTOR_PROVIDER, - TX_TRANSITION_ACTOR_SYSTEM, - TX_TRANSITION_ACTOR_OPERATOR, -]; - -const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition; - -export const txIsEnquired = tx => txLastTransition(tx) === TRANSITION_ENQUIRE; - -export const txIsRequested = tx => { - const transition = txLastTransition(tx); - return transition === TRANSITION_REQUEST || transition === TRANSITION_REQUEST_AFTER_ENQUIRY; -}; - -export const txIsAccepted = tx => txLastTransition(tx) === TRANSITION_ACCEPT; - -export const txIsDeclined = tx => txLastTransition(tx) === TRANSITION_DECLINE; - -export const txIsExpired = tx => txLastTransition(tx) === TRANSITION_EXPIRE; - -export const txIsDeclinedOrExpired = tx => txIsDeclined(tx) || txIsExpired(tx); - -export const txIsCanceled = tx => txLastTransition(tx) === TRANSITION_CANCEL; - -export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE; - -export const txHasFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx)); - -export const txIsReviewed = tx => areReviewsCompleted(txLastTransition(tx)); - propTypes.transition = shape({ createdAt: instanceOf(Date).isRequired, by: oneOf(TX_TRANSITION_ACTORS).isRequired, transition: oneOf(TRANSITIONS).isRequired, }); -const firstReviewTransitions = [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_1_BY_PROVIDER]; - -// Check if tx transition is followed by a state where -// reviews are completed -export const areReviewsCompleted = transition => { - return [ - TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, - TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, - TRANSITION_EXPIRE_REVIEW_PERIOD, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, - ].includes(transition); -}; - -export const txHasBeenAccepted = tx => { - const transition = txLastTransition(tx); - return [ - TRANSITION_ACCEPT, - TRANSITION_REVIEW_1_BY_CUSTOMER, - TRANSITION_REVIEW_1_BY_PROVIDER, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, - TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, - TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, - TRANSITION_EXPIRE_REVIEW_PERIOD, - TRANSITION_COMPLETE, - ].includes(transition); -}; - // Possible amount of stars in a review export const REVIEW_RATINGS = [1, 2, 3, 4, 5]; From 6b2b7e9b6c99d5e7b2a2879622b997529076ecf7 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 21 Jan 2019 20:36:25 +0200 Subject: [PATCH 02/12] Add txHasBeenDelivered helper function --- src/util/transaction.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/util/transaction.js b/src/util/transaction.js index cf0c693c..b5aed189 100644 --- a/src/util/transaction.js +++ b/src/util/transaction.js @@ -105,6 +105,10 @@ export const txHasBeenAccepted = tx => { ].includes(transition); }; +export const txHasBeenDelivered = tx => { + return txHasFirstReview(tx) || txIsReviewed(tx) || txIsCompleted(tx); +}; + // Roles of actors that perform transaction transitions export const TX_TRANSITION_ACTOR_CUSTOMER = 'customer'; export const TX_TRANSITION_ACTOR_PROVIDER = 'provider'; From 419dbb05d8205a7238d701c9346c941d796616d5 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Tue, 22 Jan 2019 21:44:43 +0200 Subject: [PATCH 03/12] BookingDatesForm doesn't need to test last transition. --- src/forms/BookingDatesForm/BookingDatesForm.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/forms/BookingDatesForm/BookingDatesForm.test.js b/src/forms/BookingDatesForm/BookingDatesForm.test.js index 31108a4f..bd875e77 100644 --- a/src/forms/BookingDatesForm/BookingDatesForm.test.js +++ b/src/forms/BookingDatesForm/BookingDatesForm.test.js @@ -4,7 +4,6 @@ import Decimal from 'decimal.js'; import { types as sdkTypes } from '../../util/sdkLoader'; import { renderShallow, renderDeep } from '../../util/test-helpers'; import { fakeIntl } from '../../util/test-data'; -import { TRANSITION_REQUEST } from '../../util/transaction'; import { LINE_ITEM_NIGHT } from '../../util/types'; import { dateFromAPIToLocalNoon } from '../../util/dates'; import { BookingBreakdown } from '../../components'; @@ -71,7 +70,6 @@ describe('EstimatedBreakdownMaybe', () => { expect(dateFromAPIToLocalNoon(booking.attributes.start)).toEqual(startDate); expect(dateFromAPIToLocalNoon(booking.attributes.end)).toEqual(endDate); - expect(transaction.attributes.lastTransition).toEqual(TRANSITION_REQUEST); expect(transaction.attributes.payinTotal).toEqual(new Money(2198, 'USD')); expect(transaction.attributes.payoutTotal).toEqual(new Money(2198, 'USD')); expect(transaction.attributes.lineItems).toEqual([ From c33afc9b0cef2e9055248a48d0b6dc28e7fa5f26 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Tue, 22 Jan 2019 21:46:11 +0200 Subject: [PATCH 04/12] Expose which transitions lead to request --- src/ducks/user.duck.js | 4 ++-- src/util/transaction.js | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index be852606..c37ddebc 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -3,7 +3,7 @@ import isUndefined from 'lodash/isUndefined'; import config from '../config'; import { denormalisedResponseEntities, ensureOwnListing } from '../util/data'; import { storableError } from '../util/errors'; -import { TRANSITION_REQUEST, TRANSITION_REQUEST_AFTER_ENQUIRY } from '../util/transaction'; +import { transitionsToRequested } from '../util/transaction'; import { LISTING_STATE_DRAFT } from '../util/types'; import * as log from '../util/log'; import { authInfo } from './Auth.duck'; @@ -316,7 +316,7 @@ export const fetchCurrentUserNotifications = () => (dispatch, getState, sdk) => const apiQueryParams = { only: 'sale', - last_transitions: [TRANSITION_REQUEST, TRANSITION_REQUEST_AFTER_ENQUIRY], + last_transitions: transitionsToRequested, page: 1, per_page: NOTIFICATION_PAGE_SIZE, }; diff --git a/src/util/transaction.js b/src/util/transaction.js index b5aed189..ff28dafe 100644 --- a/src/util/transaction.js +++ b/src/util/transaction.js @@ -56,10 +56,8 @@ const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition; export const txIsEnquired = tx => txLastTransition(tx) === TRANSITION_ENQUIRE; -export const txIsRequested = tx => { - const transition = txLastTransition(tx); - return transition === TRANSITION_REQUEST || transition === TRANSITION_REQUEST_AFTER_ENQUIRY; -}; +export const transitionsToRequested = [TRANSITION_REQUEST, TRANSITION_REQUEST_AFTER_ENQUIRY]; +export const txIsRequested = tx => transitionsToRequested.includes(txLastTransition(tx)); export const txIsAccepted = tx => txLastTransition(tx) === TRANSITION_ACCEPT; From 538faf945d49cb4e3bba9a177ec7eac7cea7f9e2 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Tue, 22 Jan 2019 21:48:16 +0200 Subject: [PATCH 05/12] Add tx helpers related to sending a review --- .../TransactionPage/TransactionPage.duck.js | 18 ++++++------------ src/util/transaction.js | 11 +++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/containers/TransactionPage/TransactionPage.duck.js b/src/containers/TransactionPage/TransactionPage.duck.js index 59cc018e..ac59f84e 100644 --- a/src/containers/TransactionPage/TransactionPage.duck.js +++ b/src/containers/TransactionPage/TransactionPage.duck.js @@ -7,12 +7,11 @@ import { types as sdkTypes } from '../../util/sdkLoader'; import { isTransactionsTransitionInvalidTransition, storableError } from '../../util/errors'; import { txIsEnquired, + getReview1Transition, + getReview2Transition, + txHasFirstReviewBy, TRANSITION_ACCEPT, TRANSITION_DECLINE, - TRANSITION_REVIEW_1_BY_CUSTOMER, - TRANSITION_REVIEW_1_BY_PROVIDER, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, } from '../../util/transaction'; import * as log from '../../util/log'; import { @@ -443,8 +442,7 @@ const IMAGE_VARIANTS = { // If other party has already sent a review, we need to make transition to // TRANSITION_REVIEW_2_BY_ const sendReviewAsSecond = (id, params, role, dispatch, sdk) => { - const transition = - role === CUSTOMER ? TRANSITION_REVIEW_2_BY_CUSTOMER : TRANSITION_REVIEW_2_BY_PROVIDER; + const transition = getReview2Transition(role === CUSTOMER); const include = REVIEW_TX_INCLUDES; @@ -470,8 +468,7 @@ const sendReviewAsSecond = (id, params, role, dispatch, sdk) => { // So, error is likely to happen and then we must try another state transition // by calling sendReviewAsSecond(). const sendReviewAsFirst = (id, params, role, dispatch, sdk) => { - const transition = - role === CUSTOMER ? TRANSITION_REVIEW_1_BY_CUSTOMER : TRANSITION_REVIEW_1_BY_PROVIDER; + const transition = getReview1Transition(role === CUSTOMER); const include = REVIEW_TX_INCLUDES; return sdk.transactions @@ -498,10 +495,7 @@ const sendReviewAsFirst = (id, params, role, dispatch, sdk) => { export const sendReview = (role, tx, reviewRating, reviewContent) => (dispatch, getState, sdk) => { const params = { reviewRating, reviewContent }; - const txStateOtherPartyFirst = - role === CUSTOMER - ? tx.attributes.lastTransition === TRANSITION_REVIEW_1_BY_PROVIDER - : tx.attributes.lastTransition === TRANSITION_REVIEW_1_BY_CUSTOMER; + const txStateOtherPartyFirst = txHasFirstReviewBy(tx, role !== CUSTOMER); dispatch(sendReviewRequest()); diff --git a/src/util/transaction.js b/src/util/transaction.js index ff28dafe..42e6c3ee 100644 --- a/src/util/transaction.js +++ b/src/util/transaction.js @@ -74,8 +74,19 @@ export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE; const firstReviewTransitions = [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_1_BY_PROVIDER]; export const txHasFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx)); +export const txHasFirstReviewBy = (tx, isCustomer) => + isCustomer + ? txLastTransition(tx) === TRANSITION_REVIEW_1_BY_CUSTOMER + : txLastTransition(tx) === TRANSITION_REVIEW_1_BY_PROVIDER; + export const txIsReviewed = tx => areReviewsCompleted(txLastTransition(tx)); +export const getReview1Transition = isCustomer => + isCustomer ? TRANSITION_REVIEW_1_BY_CUSTOMER : TRANSITION_REVIEW_1_BY_PROVIDER; + +export const getReview2Transition = isCustomer => + isCustomer ? TRANSITION_REVIEW_2_BY_CUSTOMER : TRANSITION_REVIEW_2_BY_PROVIDER; + // Check if tx transition is followed by a state where // reviews are completed export const areReviewsCompleted = transition => { From 32fc6ba5b1555a7b38c7d5897a00d0d74d0830c8 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Tue, 22 Jan 2019 20:10:19 +0200 Subject: [PATCH 06/12] Split TransactionPanel to dummy subcomponents and reduce direct state check agains current transaction. --- .../BookingBreakdown/BookingBreakdown.css | 4 +- .../TransactionPanel/AddressLinkMaybe.js | 29 + .../TransactionPanel/BreakdownMaybe.js | 33 + .../DetailCardHeadingsMaybe.js | 26 + .../TransactionPanel/DetailCardImage.js | 40 + .../TransactionPanel/FeedSection.js | 64 + .../TransactionPanel/PanelHeading.js | 226 ++ .../SaleActionButtonsMaybe.js | 64 + .../TransactionPanel/TransactionPanel.css | 229 +- .../TransactionPanel.helpers.js | 550 ---- .../TransactionPanel/TransactionPanel.js | 246 +- .../TransactionPanel.test.js.snap | 2264 +++++------------ 12 files changed, 1337 insertions(+), 2438 deletions(-) create mode 100644 src/components/TransactionPanel/AddressLinkMaybe.js create mode 100644 src/components/TransactionPanel/BreakdownMaybe.js create mode 100644 src/components/TransactionPanel/DetailCardHeadingsMaybe.js create mode 100644 src/components/TransactionPanel/DetailCardImage.js create mode 100644 src/components/TransactionPanel/FeedSection.js create mode 100644 src/components/TransactionPanel/PanelHeading.js create mode 100644 src/components/TransactionPanel/SaleActionButtonsMaybe.js delete mode 100644 src/components/TransactionPanel/TransactionPanel.helpers.js diff --git a/src/components/BookingBreakdown/BookingBreakdown.css b/src/components/BookingBreakdown/BookingBreakdown.css index 7ff45a13..60801bbb 100644 --- a/src/components/BookingBreakdown/BookingBreakdown.css +++ b/src/components/BookingBreakdown/BookingBreakdown.css @@ -66,7 +66,7 @@ font-weight: var(--fontWeightSemiBold); /* Move so that baseline aligns with the total price */ - padding-top: 7px; + padding-top: 6px; @media (--viewportMedium) { padding-top: 8px; @@ -77,9 +77,11 @@ @apply --marketplaceBodyFontStyles; font-weight: var(--fontWeightSemiBold); margin: 0 0 0 10px; + padding-top: 4px; @media (--viewportMedium) { margin: 2px 0 0 10px; + padding-top: 0; } } diff --git a/src/components/TransactionPanel/AddressLinkMaybe.js b/src/components/TransactionPanel/AddressLinkMaybe.js new file mode 100644 index 00000000..eba5470c --- /dev/null +++ b/src/components/TransactionPanel/AddressLinkMaybe.js @@ -0,0 +1,29 @@ +import React from 'react'; +import classNames from 'classnames'; +import { ExternalLink } from '../../components'; + +import css from './TransactionPanel.css'; + +// Functional component as a helper to build AddressLinkMaybe +const AddressLinkMaybe = props => { + const { className, rootClassName, location, geolocation, showAddress } = props; + const { address, building } = location || {}; + const { lat, lng } = geolocation || {}; + const hrefToGoogleMaps = geolocation + ? `https://maps.google.com/?q=${lat},${lng}` + : address + ? `https://maps.google.com/?q=${encodeURIComponent(address)}` + : null; + + const fullAddress = + typeof building === 'string' && building.length > 0 ? `${building}, ${address}` : address; + + const classes = classNames(rootClassName || css.address, className); + return showAddress && hrefToGoogleMaps ? ( +

+ {fullAddress} +

+ ) : null; +}; + +export default AddressLinkMaybe; diff --git a/src/components/TransactionPanel/BreakdownMaybe.js b/src/components/TransactionPanel/BreakdownMaybe.js new file mode 100644 index 00000000..c2dc26d1 --- /dev/null +++ b/src/components/TransactionPanel/BreakdownMaybe.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import config from '../../config'; +import { BookingBreakdown } from '../../components'; + +import css from './TransactionPanel.css'; + +// Functional component as a helper to build BookingBreakdown +const BreakdownMaybe = props => { + const { className, rootClassName, breakdownClassName, transaction, transactionRole } = props; + const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id; + + const classes = classNames(rootClassName || css.breakdownMaybe, className); + const breakdownClasses = classNames(breakdownClassName || css.breakdown); + + return loaded ? ( +
+

+ +

+ +
+ ) : null; +}; + +export default BreakdownMaybe; diff --git a/src/components/TransactionPanel/DetailCardHeadingsMaybe.js b/src/components/TransactionPanel/DetailCardHeadingsMaybe.js new file mode 100644 index 00000000..a58e51fa --- /dev/null +++ b/src/components/TransactionPanel/DetailCardHeadingsMaybe.js @@ -0,0 +1,26 @@ +import React from 'react'; +import AddressLinkMaybe from './AddressLinkMaybe'; + +import css from './TransactionPanel.css'; + +// Functional component as a helper to build detail card headings +const DetailCardHeadingsMaybe = props => { + const { + showDetailCardHeadings, + listingTitle, + subTitle, + location, + geolocation, + showAddress, + } = props; + + return showDetailCardHeadings ? ( +
+

{listingTitle}

+

{subTitle}

+ +
+ ) : null; +}; + +export default DetailCardHeadingsMaybe; diff --git a/src/components/TransactionPanel/DetailCardImage.js b/src/components/TransactionPanel/DetailCardImage.js new file mode 100644 index 00000000..abe6607e --- /dev/null +++ b/src/components/TransactionPanel/DetailCardImage.js @@ -0,0 +1,40 @@ +import React from 'react'; +import classNames from 'classnames'; +import { AvatarMedium, ResponsiveImage } from '../../components'; + +import css from './TransactionPanel.css'; + +// Functional component as a helper to build AddressLinkMaybe +const DetailCardImage = props => { + const { + className, + rootClassName, + avatarWrapperClassName, + listingTitle, + image, + provider, + isCustomer, + } = props; + const classes = classNames(rootClassName || css.detailCardImageWrapper, className); + return ( + +
+
+ +
+
+ {isCustomer ? ( +
+ +
+ ) : null} +
+ ); +}; + +export default DetailCardImage; diff --git a/src/components/TransactionPanel/FeedSection.js b/src/components/TransactionPanel/FeedSection.js new file mode 100644 index 00000000..51d7ccfb --- /dev/null +++ b/src/components/TransactionPanel/FeedSection.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import { ActivityFeed } from '../../components'; + +import css from './TransactionPanel.css'; + +// Functional component as a helper to build ActivityFeed section +const FeedSection = props => { + const { + className, + rootClassName, + currentTransaction, + currentUser, + fetchMessagesError, + fetchMessagesInProgress, + initialMessageFailed, + messages, + oldestMessagePageFetched, + onShowMoreMessages, + onOpenReviewModal, + totalMessagePages, + } = props; + + const txTransitions = currentTransaction.attributes.transitions + ? currentTransaction.attributes.transitions + : []; + const hasOlderMessages = totalMessagePages > oldestMessagePageFetched; + + const showFeed = + messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError; + + const classes = classNames(rootClassName || css.feedContainer, className); + + return showFeed ? ( +
+

+ +

+ {initialMessageFailed ? ( +

+ +

+ ) : null} + {fetchMessagesError ? ( +

+ +

+ ) : null} + +
+ ) : null; +}; + +export default FeedSection; diff --git a/src/components/TransactionPanel/PanelHeading.js b/src/components/TransactionPanel/PanelHeading.js new file mode 100644 index 00000000..b41275f9 --- /dev/null +++ b/src/components/TransactionPanel/PanelHeading.js @@ -0,0 +1,226 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import { createSlug, stringify } from '../../util/urlHelpers'; +import { NamedLink } from '../../components'; + +import css from './TransactionPanel.css'; + +export const HEADING_ENQUIRED = 'enquired'; +export const HEADING_REQUESTED = 'requested'; +export const HEADING_ACCEPTED = 'accepted'; +export const HEADING_DECLINED = 'declined'; +export const HEADING_CANCELED = 'canceled'; +export const HEADING_DELIVERED = 'deliveded'; + +const createListingLink = (listingId, label, listingDeleted, searchParams = {}, className = '') => { + if (!listingDeleted) { + const params = { id: listingId, slug: createSlug(label) }; + const to = { search: stringify(searchParams) }; + return ( + + {label} + + ); + } else { + return ; + } +}; + +const ListingDeletedInfoMaybe = props => { + return props.listingDeleted ? ( +

+ +

+ ) : null; +}; + +const HeadingCustomer = props => { + const { className, id, values, listingDeleted } = props; + return ( + +

+ + + +

+ +
+ ); +}; + +const HeadingCustomerWithSubtitle = props => { + const { className, id, values, subtitleId, subtitleValues, children, listingDeleted } = props; + return ( + +

+ + + + +

+ {children} + +
+ ); +}; + +const CustomerBannedInfoMaybe = props => { + return props.isCustomerBanned ? ( +

+ +

+ ) : null; +}; + +const HeadingProvider = props => { + const { className, id, values, isCustomerBanned } = props; + return ( + +

+ + + +

+ +
+ ); +}; + +// Functional component as a helper to choose and show Order or Sale heading info: +// title, subtitle, and message +const PanelHeading = props => { + const { + className, + rootClassName, + panelHeadingState, + customerName, + providerName, + listingId, + listingTitle, + listingDeleted, + isCustomerBanned, + } = props; + + const isCustomer = props.transactionRole === 'customer'; + + const defaultRootClassName = isCustomer ? css.headingOrder : css.headingSale; + const titleClasses = classNames(rootClassName || defaultRootClassName, className); + const listingLink = createListingLink(listingId, listingTitle, listingDeleted); + + switch (panelHeadingState) { + case HEADING_ENQUIRED: + return isCustomer ? ( + + ) : ( + + ); + case HEADING_REQUESTED: + return isCustomer ? ( + + {!listingDeleted ? ( +

+ +

+ ) : null} +
+ ) : ( + + {!isCustomerBanned ? ( +

+ +

+ ) : null} +
+ ); + case HEADING_ACCEPTED: + return isCustomer ? ( + + ) : ( + + ); + case HEADING_DECLINED: + return isCustomer ? ( + + ) : ( + + ); + case HEADING_CANCELED: + return isCustomer ? ( + + ) : ( + + ); + case HEADING_DELIVERED: + return isCustomer ? ( + + ) : ( + + ); + default: + console.warning('Unknown state given to panel heading.'); + return null; + } +}; + +export default PanelHeading; diff --git a/src/components/TransactionPanel/SaleActionButtonsMaybe.js b/src/components/TransactionPanel/SaleActionButtonsMaybe.js new file mode 100644 index 00000000..6951afd9 --- /dev/null +++ b/src/components/TransactionPanel/SaleActionButtonsMaybe.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import { PrimaryButton, SecondaryButton } from '../../components'; + +import css from './TransactionPanel.css'; + +// Functional component as a helper to build ActionButtons for +// provider when state is preauthorized +const SaleActionButtonsMaybe = props => { + const { + className, + rootClassName, + showButtons, + acceptInProgress, + declineInProgress, + acceptSaleError, + declineSaleError, + onAcceptSale, + onDeclineSale, + } = props; + + const buttonsDisabled = acceptInProgress || declineInProgress; + + const acceptErrorMessage = acceptSaleError ? ( +

+ +

+ ) : null; + const declineErrorMessage = declineSaleError ? ( +

+ +

+ ) : null; + + const classes = classNames(rootClassName || css.actionButtons, className); + + return showButtons ? ( +
+
+ {acceptErrorMessage} + {declineErrorMessage} +
+
+ + + + + + +
+
+ ) : null; +}; + +export default SaleActionButtonsMaybe; diff --git a/src/components/TransactionPanel/TransactionPanel.css b/src/components/TransactionPanel/TransactionPanel.css index bd0bc237..3c71b6fa 100644 --- a/src/components/TransactionPanel/TransactionPanel.css +++ b/src/components/TransactionPanel/TransactionPanel.css @@ -29,14 +29,17 @@ } } -.imageWrapperMobile { +/* DetailCardImage subcomponent */ +.detailCardImageWrapper { /* Layout */ - display: block; + display: none; width: 100%; position: relative; @media (--viewportLarge) { - display: none; + display: block; + max-height: 268px; + overflow-y: hidden; } } @@ -60,37 +63,51 @@ } } -.avatarWrapperMobile { +.avatarWrapper { /* Position (over the listing image)*/ margin-left: 24px; - margin-top: -31px; + margin-top: -30px; /* Rendering context to the same lavel as listing image */ position: relative; - - /* Layout */ - display: block; -} - -.avatarWrapperCustomerDesktop { - display: none; - composes: avatarWrapperMobile; - - @media (--viewportLarge) { - display: block; - margin-left: 48px; - } -} - -.avatarMobile { /* Flex would give too much width by default. */ width: 60px; + @media (--viewportMedium) { + margin-top: -32px; + padding: 2px 0; + } + @media (--viewportLarge) { + margin-left: 48px; + width: unset; + padding: 2px 0; + } +} + +/* Passed-in props for DetailCardImage subcomponent */ +.imageWrapperMobile { @media (--viewportLarge) { display: none; } } +.avatarWrapperMobile { + composes: avatarWrapper; + + @media (--viewportLarge) { + display: none; + } +} + +.avatarWrapperDesktop { + composes: avatarWrapper; + display: none; + + @media (--viewportLarge) { + display: block; + } +} + .avatarWrapperProviderDesktop { display: none; @@ -105,30 +122,37 @@ } } +/* PanelHeadings subcomponent */ .headingOrder { margin: 29px 24px 0 24px; @media (--viewportMedium) { - margin: 25px 24px 0 24px; max-width: 80%; + margin: 24px 24px 0 24px; + padding: 1px 0 7px 0; } @media (--viewportLarge) { max-width: 100%; margin: 152px 0 0 0; + padding: 0; } } .headingSale { - margin: 22px 24px 0 24px; + margin: 18px 24px 0 24px; + padding: 5px 0 1px 0; @media (--viewportMedium) { max-width: 80%; + margin: 24px 24px 0 24px; + padding: 1px 0 7px 0; } @media (--viewportLarge) { max-width: 100%; margin: 42px 0 0 0; + padding: 0; } } @@ -147,28 +171,30 @@ } } -.transitionDate { - white-space: nowrap; -} - +/* Container for booking details in mobile layout */ .bookingDetailsMobile { margin-top: 47px; @media (--viewportMedium) { - margin-top: 43px; + margin-top: 40px; + padding: 4px 0 0px 0; } @media (--viewportLarge) { display: none; } } +/* "aside" section in desktop layout */ .asideDesktop { margin: 1px 0 0 0; + /** + * Aside is visible on mobile layout too, since it includes BookingPanel component. + * It might get rendered as a Modal in mobile layout. + */ + @media (--viewportLarge) { - margin-top: 123px; - margin-left: 0; - margin-right: 0; + margin: 123px 0 0 0; } } @@ -184,26 +210,13 @@ } } -.detailCardImageWrapper { - display: none; - - /* Layout */ - width: 100%; - position: relative; - - @media (--viewportLarge) { - display: block; - } -} - +/* DetailCardHeadingsMaybe subcomponent */ .detailCardHeadings { display: none; @media (--viewportLarge) { display: block; - margin-left: 48px; - margin-right: 48px; - margin-bottom: 37px; + margin: 0 48px 33px 48px; } } @@ -228,16 +241,25 @@ } } +/* AddressLinkMaybe subcomponent */ .address { @apply --marketplaceH5FontStyles; color: var(--matterColorAnti); margin: 0; } -.addressMobileWrapper { +/* Passed-in props for AddressLinkMaybe subcomponent */ +.addressMobile { + @apply --marketplaceH5FontStyles; + color: var(--matterColorAnti); margin: 0 24px 24px 24px; } +/* BreakdownMaybe subcomponent */ +.breakdownMaybe { + /* default "root" class for breakdown container */ +} + .bookingBreakdownTitle { /* Font */ color: var(--matterColorAnti); @@ -246,14 +268,8 @@ @media (--viewportLarge) { margin: 37px 48px 26px 48px; - } -} - -.breakdownContainer { - display: none; - - @media (--viewportLarge) { - display: block; + margin: 32px 48px 24px 48px; + padding: 4px 0 4px 0; } } @@ -264,27 +280,12 @@ margin: 18px 24px 0 24px; } @media (--viewportLarge) { - margin: 14px 48px 47px 48px; - } -} - -.feedContainer { - margin: 46px 24px 0 24px; - - @media (--viewportMedium) { - margin: 46px 24px 0 24px; - } - @media (--viewportLarge) { - margin: 43px 0 0 0; - } -} - -.feedContainerWithInfoAbove { - @media (--viewportLarge) { - margin: 38px 0 0 0; + margin: 24px 48px 47px 48px; + padding: 6px 0 2px 0; } } +/* FeedSection subcomponent */ .feedHeading { color: var(--matterColorAnti); margin: 0; @@ -298,6 +299,32 @@ margin-top: 20px; } +.messageError { + color: var(--failColor); + margin: 13px 0 22px 0; + + @media (--viewportMedium) { + margin: 13px 0 23px 0; + } + + @media (--viewportLarge) { + margin: 12px 0 23px 0; + } +} + +/* Passed-in props for FeedSection subcomponent */ +.feedContainer { + margin: 46px 24px 0 24px; + + @media (--viewportMedium) { + margin: 40px 24px 0 24px; + } + @media (--viewportLarge) { + margin: 43px 0 0 0; + } +} + +/* Prop to be passed to SendMessageForm component */ .sendMessageForm { position: relative; margin: 46px 24px 0 24px; @@ -311,12 +338,7 @@ } } -.requestToBookButton { - @apply --marketplaceButtonStylesPrimary; - - text-decoration: none; -} - +/* SaleActionButtonsMaybe subcomponent */ .actionButtons { /* Position action button row above the footer */ z-index: 9; @@ -342,23 +364,6 @@ } } -.mobileActionButtons { - display: block; - - @media (--viewportLarge) { - display: none; - } -} - -.desktopActionButtons { - display: none; - - @media (--viewportLarge) { - display: block; - margin-bottom: 48px; - } -} - .actionButtonWrapper { width: 100%; display: flex; @@ -380,19 +385,6 @@ } } -.messageError { - color: var(--failColor); - margin: 13px 0 22px 0; - - @media (--viewportMedium) { - margin: 13px 0 23px 0; - } - - @media (--viewportLarge) { - margin: 12px 0 23px 0; - } -} - .actionError { @apply --marketplaceH5FontStyles; color: var(--failColor); @@ -416,6 +408,25 @@ } } +/* Container for SaleActionButtonsMaybe subcomponent */ +.mobileActionButtons { + display: block; + + @media (--viewportLarge) { + display: none; + } +} + +.desktopActionButtons { + display: none; + + @media (--viewportLarge) { + display: block; + margin-bottom: 48px; + } +} + +/* BookingPanel subcompnent */ .bookingPanel { margin: 16px 48px 48px 48px; } diff --git a/src/components/TransactionPanel/TransactionPanel.helpers.js b/src/components/TransactionPanel/TransactionPanel.helpers.js deleted file mode 100644 index 022614d6..00000000 --- a/src/components/TransactionPanel/TransactionPanel.helpers.js +++ /dev/null @@ -1,550 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; -import { - txHasBeenAccepted, - txHasFirstReview, - txIsAccepted, - txIsCanceled, - txIsCompleted, - txIsDeclined, - txIsEnquired, - txIsExpired, - txIsRequested, - txIsReviewed, -} from '../../util/types'; -import { userDisplayName } from '../../util/data'; -import { createSlug, stringify } from '../../util/urlHelpers'; -import { - ActivityFeed, - BookingBreakdown, - BookingPanel, - ExternalLink, - NamedLink, - PrimaryButton, - SecondaryButton, -} from '../../components'; -import config from '../../config'; - -import css from './TransactionPanel.css'; - -// Functional component as a helper to build ActivityFeed section -export const FeedSection = props => { - const { - className, - rootClassName, - currentTransaction, - currentUser, - fetchMessagesError, - fetchMessagesInProgress, - initialMessageFailed, - messages, - oldestMessagePageFetched, - onShowMoreMessages, - onOpenReviewModal, - totalMessagePages, - } = props; - - const txTransitions = currentTransaction.attributes.transitions - ? currentTransaction.attributes.transitions - : []; - const hasOlderMessages = totalMessagePages > oldestMessagePageFetched; - - const showFeed = - messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError; - - const classes = classNames(rootClassName || css.feedContainer, className); - - return showFeed ? ( -
-

- -

- {initialMessageFailed ? ( -

- -

- ) : null} - {fetchMessagesError ? ( -

- -

- ) : null} - { - onShowMoreMessages(currentTransaction.id); - }} - fetchMessagesInProgress={fetchMessagesInProgress} - /> -
- ) : null; -}; - -// Functional component as a helper to build AddressLinkMaybe -export const AddressLinkMaybe = props => { - const { transaction, transactionRole, currentListing } = props; - - const isCustomer = transactionRole === 'customer'; - const txIsAcceptedForCustomer = isCustomer && txHasBeenAccepted(transaction); - - const publicData = currentListing.attributes.publicData || {}; - const { address, building } = publicData.location || {}; - const geolocation = currentListing.attributes.geolocation; - - const { lat, lng } = geolocation || {}; - const hrefToGoogleMaps = geolocation - ? `https://maps.google.com/?q=${lat},${lng}` - : address - ? `https://maps.google.com/?q=${encodeURIComponent(address)}` - : null; - - const fullAddress = - typeof building === 'string' && building.length > 0 ? `${building}, ${address}` : address; - - return txIsAcceptedForCustomer && hrefToGoogleMaps ? ( -

- {fullAddress} -

- ) : null; -}; - -// Functional component as a helper to build BookingBreakdown -export const BreakdownMaybe = props => { - const { className, rootClassName, breakdownClassName, transaction, transactionRole } = props; - const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id; - - const classes = classNames(rootClassName || className); - const breakdownClasses = classNames(css.breakdown, breakdownClassName); - - return loaded ? ( -
-

- -

- -
- ) : null; -}; - -const createListingLink = (listing, label, searchParams = {}, className = '') => { - const listingLoaded = !!listing.id; - - if (listingLoaded && !listing.attributes.deleted) { - const title = listing.attributes.title; - const params = { id: listing.id.uuid, slug: createSlug(title) }; - const to = { search: stringify(searchParams) }; - return ( - - {label} - - ); - } else { - return ; - } -}; - -// Functional component as a helper to build detail card headings -export const DetailCardHeadingsMaybe = props => { - const { transaction, transactionRole, listing, listingTitle, subTitle } = props; - - const isCustomer = transactionRole === 'customer'; - const canShowDetailCardHeadings = isCustomer && !txIsEnquired(transaction); - - return canShowDetailCardHeadings ? ( -
-

{listingTitle}

-

{subTitle}

- -
- ) : null; -}; - -// Functional component as a helper to build a BookingPanel -export const BookingPanelMaybe = props => { - const { - authorDisplayName, - transaction, - transactionRole, - listing, - listingTitle, - subTitle, - provider, - onSubmit, - onManageDisableScrolling, - timeSlots, - fetchTimeSlotsError, - } = props; - - const isProviderLoaded = !!provider.id; - const isProviderBanned = isProviderLoaded && provider.attributes.banned; - const isCustomer = transactionRole === 'customer'; - const canShowBookingPanel = isCustomer && txIsEnquired(transaction) && !isProviderBanned; - - return canShowBookingPanel ? ( - console.log('submit')} - title={listingTitle} - subTitle={subTitle} - authorDisplayName={authorDisplayName} - onSubmit={onSubmit} - onManageDisableScrolling={onManageDisableScrolling} - timeSlots={timeSlots} - fetchTimeSlotsError={fetchTimeSlotsError} - /> - ) : null; -}; - -// Functional component as a helper to build ActionButtons for -// provider when state is preauthorized -export const SaleActionButtonsMaybe = props => { - const { - className, - rootClassName, - canShowButtons, - transaction, - acceptInProgress, - declineInProgress, - acceptSaleError, - declineSaleError, - onAcceptSale, - onDeclineSale, - } = props; - - const buttonsDisabled = acceptInProgress || declineInProgress; - - const acceptErrorMessage = acceptSaleError ? ( -

- -

- ) : null; - const declineErrorMessage = declineSaleError ? ( -

- -

- ) : null; - - const classes = classNames(rootClassName || css.actionButtons, className); - - return canShowButtons ? ( -
-
- {acceptErrorMessage} - {declineErrorMessage} -
-
- onDeclineSale(transaction.id)} - > - - - onAcceptSale(transaction.id)} - > - - -
-
- ) : null; -}; - -// Functional component as a helper to build order title -export const OrderTitle = props => { - const { - className, - rootClassName, - transaction, - customerDisplayName: customerName, - currentListing, - listingTitle, - } = props; - const listingLink = createListingLink(currentListing, listingTitle); - - const classes = classNames(rootClassName || css.headingOrder, className); - - if (txIsEnquired(transaction)) { - return ( -

- - - -

- ); - } else if (txIsRequested(transaction)) { - return ( -

- - - - -

- ); - } else if (txIsAccepted(transaction)) { - return ( -

- - - - -

- ); - } else if (txIsDeclined(transaction)) { - return ( -

- -

- ); - } else if (txIsExpired(transaction)) { - return ( -

- -

- ); - } else if (txIsCanceled(transaction)) { - return ( -

- -

- ); - } else if ( - txIsCompleted(transaction) || - txHasFirstReview(transaction) || - txIsReviewed(transaction) - ) { - return ( -

- -

- ); - } else { - return null; - } -}; - -// Functional component as a helper to build order message below title -export const OrderMessage = props => { - const { - className, - rootClassName, - transaction, - authorDisplayName: providerName, - listingDeleted, - } = props; - const classes = classNames(rootClassName || css.transactionInfoMessage, className); - - if (!listingDeleted && txIsRequested(transaction)) { - return ( -

- -

- ); - } else if (listingDeleted) { - return ( -

- -

- ); - } - return null; -}; - -// Functional component as a helper to build sale title -export const SaleTitle = props => { - const { - className, - rootClassName, - transaction, - customerDisplayName: customerName, - currentListing, - listingTitle, - } = props; - const listingLink = createListingLink(currentListing, listingTitle); - - const classes = classNames(rootClassName || css.headingSale, className); - - if (txIsEnquired(transaction)) { - return ( -

- -

- ); - } else if (txIsRequested(transaction)) { - return ( -

- -

- ); - } else if (txIsAccepted(transaction)) { - return ( -

- -

- ); - } else if (txIsDeclined(transaction)) { - return ( -

- -

- ); - } else if (txIsExpired(transaction)) { - return ( -

- -

- ); - } else if (txIsCanceled(transaction)) { - return ( -

- -

- ); - } else if ( - txIsCompleted(transaction) || - txHasFirstReview(transaction) || - txIsReviewed(transaction) - ) { - return ( -

- -

- ); - } else { - return null; - } -}; - -// Functional component as a helper to build sale message below title -export const SaleMessage = props => { - const { - className, - rootClassName, - transaction, - customerDisplayName: customerName, - isCustomerBanned, - } = props; - const classes = classNames(rootClassName || css.transactionInfoMessage, className); - - if (!isCustomerBanned && txIsRequested(transaction)) { - return ( -

- -

- ); - } else if (isCustomerBanned) { - return ( -

- -

- ); - } - return null; -}; - -// Functional component as a helper to choose and show Order or Sale title -export const TransactionPageTitle = props => { - return props.transactionRole === 'customer' ? ( - - ) : ( - - ); -}; - -// Functional component as a helper to choose and show Order or Sale message -export const TransactionPageMessage = props => { - return props.transactionRole === 'customer' ? ( - - ) : ( - - ); -}; - -// Helper function to get display names for different roles -export const displayNames = ( - currentUser, - currentProvider, - currentCustomer, - bannedUserDisplayName -) => { - const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName); - const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName); - - let otherUserDisplayName = ''; - const currentUserIsCustomer = - currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid; - const currentUserIsProvider = - currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid; - - if (currentUserIsCustomer) { - otherUserDisplayName = authorDisplayName; - } else if (currentUserIsProvider) { - otherUserDisplayName = customerDisplayName; - } - - return { - authorDisplayName, - customerDisplayName, - otherUserDisplayName, - }; -}; diff --git a/src/components/TransactionPanel/TransactionPanel.js b/src/components/TransactionPanel/TransactionPanel.js index c068bb2d..b402bcc3 100644 --- a/src/components/TransactionPanel/TransactionPanel.js +++ b/src/components/TransactionPanel/TransactionPanel.js @@ -2,29 +2,64 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { injectIntl, intlShape } from 'react-intl'; import classNames from 'classnames'; -import { txIsRequested, LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types'; -import { ensureListing, ensureTransaction, ensureUser } from '../../util/data'; +import { + txIsAccepted, + txIsCanceled, + txIsDeclinedOrExpired, + txIsEnquired, + txIsRequested, + txHasBeenDelivered, +} from '../../util/transaction'; +import { LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types'; +import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data'; import { isMobileSafari } from '../../util/userAgent'; import { formatMoney } from '../../util/currency'; -import { AvatarMedium, AvatarLarge, ResponsiveImage, ReviewModal } from '../../components'; +import { AvatarLarge, BookingPanel, ReviewModal } from '../../components'; import { SendMessageForm } from '../../forms'; import config from '../../config'; // These are internal components that make this file more readable. -import { - AddressLinkMaybe, - BookingPanelMaybe, - BreakdownMaybe, - DetailCardHeadingsMaybe, - FeedSection, - SaleActionButtonsMaybe, - TransactionPageTitle, - TransactionPageMessage, - displayNames, -} from './TransactionPanel.helpers'; +import AddressLinkMaybe from './AddressLinkMaybe'; +import BreakdownMaybe from './BreakdownMaybe'; +import DetailCardHeadingsMaybe from './DetailCardHeadingsMaybe'; +import DetailCardImage from './DetailCardImage'; +import FeedSection from './FeedSection'; +import SaleActionButtonsMaybe from './SaleActionButtonsMaybe'; +import PanelHeading, { + HEADING_ENQUIRED, + HEADING_REQUESTED, + HEADING_ACCEPTED, + HEADING_DECLINED, + HEADING_CANCELED, + HEADING_DELIVERED, +} from './PanelHeading'; import css from './TransactionPanel.css'; +// Helper function to get display names for different roles +const displayNames = (currentUser, currentProvider, currentCustomer, bannedUserDisplayName) => { + const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName); + const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName); + + let otherUserDisplayName = ''; + const currentUserIsCustomer = + currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid; + const currentUserIsProvider = + currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid; + + if (currentUserIsCustomer) { + otherUserDisplayName = authorDisplayName; + } else if (currentUserIsProvider) { + otherUserDisplayName = customerDisplayName; + } + + return { + authorDisplayName, + customerDisplayName, + otherUserDisplayName, + }; +}; + export class TransactionPanelComponent extends Component { constructor(props) { super(props); @@ -147,7 +182,48 @@ export class TransactionPanelComponent extends Component { const listingDeleted = listingLoaded && currentListing.attributes.deleted; const customerLoaded = !!currentCustomer.id; const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned; - const canShowSaleButtons = isProvider && txIsRequested(currentTransaction) && !isCustomerBanned; + const isProviderLoaded = !!currentProvider.id; + const isProviderBanned = isProviderLoaded && currentProvider.attributes.banned; + + const stateDataFn = tx => { + if (txIsEnquired(tx)) { + return { + headingState: HEADING_ENQUIRED, + showBookingPanel: isCustomer && !isProviderBanned, + }; + } else if (txIsRequested(tx)) { + return { + headingState: HEADING_REQUESTED, + showDetailCardHeadings: isCustomer, + showSaleButtons: isProvider && !isCustomerBanned, + }; + } else if (txIsAccepted(tx)) { + return { + headingState: HEADING_ACCEPTED, + showDetailCardHeadings: isCustomer, + showAddress: isCustomer, + }; + } else if (txIsDeclinedOrExpired(tx)) { + return { + headingState: HEADING_DECLINED, + showDetailCardHeadings: isCustomer, + }; + } else if (txIsCanceled(tx)) { + return { + headingState: HEADING_CANCELED, + showDetailCardHeadings: isCustomer, + }; + } else if (txHasBeenDelivered(tx)) { + return { + headingState: HEADING_DELIVERED, + showDetailCardHeadings: isCustomer, + showAddress: isCustomer, + }; + } else { + return { headingState: 'unknown' }; + } + }; + const stateData = stateDataFn(currentTransaction); const bannedUserDisplayName = intl.formatMessage({ id: 'TransactionPanel.bannedUserDisplayName', @@ -163,6 +239,8 @@ export class TransactionPanelComponent extends Component { bannedUserDisplayName ); + const { publicData = {}, geolocation } = currentListing.attributes; + const location = publicData.location || {}; const listingTitle = currentListing.attributes.deleted ? deletedListingTitle : currentListing.attributes.title; @@ -185,19 +263,15 @@ export class TransactionPanelComponent extends Component { const firstImage = currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null; - const actionButtonClasses = classNames(css.actionButtons); - const saleButtons = ( onAcceptSale(currentTransaction.id)} + onDeclineSale={() => onDeclineSale(currentTransaction.id)} /> ); @@ -206,68 +280,49 @@ export class TransactionPanelComponent extends Component { { name: otherUserDisplayName } ); - const sendMessageFormClasses = classNames(css.sendMessageForm); - - const showInfoMessage = listingDeleted || (!listingDeleted && txIsRequested(transaction)); // !!orderInfoMessage; - - const feedContainerClasses = classNames(css.feedContainer, { - [css.feedContainerWithInfoAbove]: showInfoMessage, - }); - const classes = classNames(rootClassName || css.root, className); return (
-
-
- -
-
-
- -
+ {isProvider ? (
) : null} - -
-
- -
+
onShowMoreMessages(currentTransaction.id)} totalMessagePages={totalMessagePages} /> - {canShowSaleButtons ? ( + {stateData.showSaleButtons ? (
{saleButtons}
) : null}
-
-
- -
-
- {isCustomer ? ( -
- -
- ) : null} + - + {stateData.showBookingPanel ? ( + + ) : null} - {canShowSaleButtons ? ( + {stateData.showSaleButtons ? (
{saleButtons}
) : null}
diff --git a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap index 4a0ea83d..aaca233c 100644 --- a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap +++ b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap @@ -6,75 +6,46 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = ` >
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + showAddress={false} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + showAddress={false} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + showAddress={false} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + showAddress={false} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + showAddress={false} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + showAddress={false} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + />
-
-
- -
-
- - +
-
-
- -
-
-
- -
+ } + />
-
- -
+ } + location={Object {}} + />
-
-
- -
-
- - + Date: Wed, 23 Jan 2019 13:27:22 +0200 Subject: [PATCH 07/12] Refactor InboxPage: use txHasBeenDelivered function, rename css classes, add txIsRequested check and handle unknown transition separately, filter unknown transitions when fetching data, don't render InboxItem if latest transition is unknown, update InboxPage tests. --- src/containers/InboxPage/InboxPage.css | 68 +++----- src/containers/InboxPage/InboxPage.duck.js | 2 + src/containers/InboxPage/InboxPage.js | 151 ++++++++++-------- src/containers/InboxPage/InboxPage.test.js | 8 +- .../__snapshots__/InboxPage.test.js.snap | 36 +++++ 5 files changed, 151 insertions(+), 114 deletions(-) diff --git a/src/containers/InboxPage/InboxPage.css b/src/containers/InboxPage/InboxPage.css index 058f0e19..6110de9b 100644 --- a/src/containers/InboxPage/InboxPage.css +++ b/src/containers/InboxPage/InboxPage.css @@ -331,8 +331,25 @@ /* Transaction status affects to certain font colors and weights */ -.nameEnquired, -.namePending { +.stateName { + /* This class is empty on purpose, it is used below for banned users */ +} + +.stateActionNeeded { + font-weight: var(--fontWeightSemiBold); + color: var(--attentionColor); +} + +.stateNoActionNeeded { + color: var(--matterColorAnti); +} + +.stateSucces { + font-weight: var(--fontWeightSemiBold); + color: var(--successColor); +} + +.nameEmphasized { font-weight: var(--fontWeightBold); /* baseline alignment fixes */ @@ -340,39 +357,11 @@ margin-bottom: 1px; } -.stateName { - /* This class is empty on purpose, it is used below for banned users */ -} - -.stateEnquired, -.stateRequested, -.statePending { - font-weight: var(--fontWeightSemiBold); - color: var(--attentionColor); -} - -.stateAccepted { - font-weight: var(--fontWeightSemiBold); - color: var(--successColor); -} - -.stateCanceled, -.stateDeclined, -.stateDelivered { - color: var(--matterColorAnti); -} - -.nameEnquiredOrder, -.nameRequested, -.nameDeclined, -.nameCanceled, -.nameAccepted, -.nameDelivered { +.nameNotEmphasized { font-weight: var(--fontWeightMedium); } -.bookingEnquired, -.bookingPending { +.bookingActionNeeded { color: var(--matterColor); margin-top: 4px; @@ -381,25 +370,16 @@ } } -.bookingRequested, -.bookingCanceled, -.bookingDeclined, -.bookingAccepted, -.bookingDelivered { +.bookingNoActionNeeded { color: var(--matterColorAnti); } -.lastTransitionedAtEnquired, -.lastTransitionedAtPending, -.lastTransitionedAtRequested { +.lastTransitionedAtEmphasized { color: var(--matterColor); font-weight: var(--fontWeightMedium); } -.lastTransitionedAtCanceled, -.lastTransitionedAtAccepted, -.lastTransitionedAtDeclined, -.lastTransitionedAtDelivered { +.lastTransitionedAtNotEmphasized { color: var(--matterColorAnti); } diff --git a/src/containers/InboxPage/InboxPage.duck.js b/src/containers/InboxPage/InboxPage.duck.js index 0276ec45..3e94cedc 100644 --- a/src/containers/InboxPage/InboxPage.duck.js +++ b/src/containers/InboxPage/InboxPage.duck.js @@ -2,6 +2,7 @@ import reverse from 'lodash/reverse'; import sortBy from 'lodash/sortBy'; import { storableError } from '../../util/errors'; import { parse } from '../../util/urlHelpers'; +import { TRANSITIONS } from '../../util/transaction'; import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck'; const sortedTransactions = txs => @@ -91,6 +92,7 @@ export const loadData = (params, search) => (dispatch, getState, sdk) => { const apiQueryParams = { only: onlyFilter, + lastTransitions: TRANSITIONS, include: ['provider', 'provider.profileImage', 'customer', 'customer.profileImage', 'booking'], 'fields.image': ['variants.square-small', 'variants.square-small2x'], page, diff --git a/src/containers/InboxPage/InboxPage.js b/src/containers/InboxPage/InboxPage.js index fdf12722..f0e95c16 100644 --- a/src/containers/InboxPage/InboxPage.js +++ b/src/containers/InboxPage/InboxPage.js @@ -6,14 +6,12 @@ import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import moment from 'moment'; import classNames from 'classnames'; import { - txHasFirstReview, txIsAccepted, txIsCanceled, - txIsCompleted, txIsDeclinedOrExpired, txIsEnquired, txIsRequested, - txIsReviewed, + txHasBeenDelivered, } from '../../util/transaction'; import { LINE_ITEM_DAY, LINE_ITEM_UNITS, propTypes } from '../../util/types'; import { formatMoney } from '../../util/currency'; @@ -55,73 +53,85 @@ const formatDate = (intl, date) => { }; // Translated name of the state of the given transaction -const txState = (intl, tx, isOrder) => { - if (txIsAccepted(tx)) { +export const txState = (intl, tx, type) => { + const isOrder = type === 'order'; + + if (txIsEnquired(tx)) { return { - nameClassName: css.nameAccepted, - bookingClassName: css.bookingAccepted, - lastTransitionedAtClassName: css.lastTransitionedAtAccepted, - stateClassName: css.stateAccepted, - state: intl.formatMessage({ - id: 'InboxPage.stateAccepted', - }), - }; - } else if (txIsDeclinedOrExpired(tx)) { - return { - nameClassName: css.nameDeclined, - bookingClassName: css.bookingDeclined, - lastTransitionedAtClassName: css.lastTransitionedAtDeclined, - stateClassName: css.stateDeclined, - state: intl.formatMessage({ - id: 'InboxPage.stateDeclined', - }), - }; - } else if (txIsCanceled(tx)) { - return { - nameClassName: css.nameCanceled, - bookingClassName: css.bookingCanceled, - lastTransitionedAtClassName: css.lastTransitionedAtCanceled, - stateClassName: css.stateCanceled, - state: intl.formatMessage({ - id: 'InboxPage.stateCanceled', - }), - }; - } else if (txIsCompleted(tx) || txHasFirstReview(tx) || txIsReviewed(tx)) { - return { - nameClassName: css.nameDelivered, - bookingClassName: css.bookingDelivered, - lastTransitionedAtClassName: css.lastTransitionedAtDelivered, - stateClassName: css.stateDelivered, - state: intl.formatMessage({ - id: 'InboxPage.stateDelivered', - }), - }; - } else if (txIsEnquired(tx)) { - return { - nameClassName: isOrder ? css.nameEnquiredOrder : css.nameEnquired, - bookingClassName: css.bookingEnquired, - lastTransitionedAtClassName: css.lastTransitionedAtEnquired, - stateClassName: css.stateEnquired, + nameClassName: isOrder ? css.nameNotEmphasized : css.nameEmphasized, + bookingClassName: css.bookingActionNeeded, + lastTransitionedAtClassName: css.lastTransitionedAtEmphasized, + stateClassName: css.stateActionNeeded, state: intl.formatMessage({ id: 'InboxPage.stateEnquiry', }), }; + } else if (txIsRequested(tx)) { + const requested = isOrder + ? { + nameClassName: css.nameNotEmphasized, + bookingClassName: css.bookingNoActionNeeded, + lastTransitionedAtClassName: css.lastTransitionedAtEmphasized, + stateClassName: css.stateActionNeeded, + state: intl.formatMessage({ + id: 'InboxPage.stateRequested', + }), + } + : { + nameClassName: css.nameEmphasized, + bookingClassName: css.bookingActionNeeded, + lastTransitionedAtClassName: css.lastTransitionedAtEmphasized, + stateClassName: css.stateActionNeeded, + state: intl.formatMessage({ + id: 'InboxPage.statePending', + }), + }; + + return requested; + } else if (txIsDeclinedOrExpired(tx)) { + return { + nameClassName: css.nameNotEmphasized, + bookingClassName: css.bookingNoActionNeeded, + lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized, + stateClassName: css.stateNoActionNeeded, + state: intl.formatMessage({ + id: 'InboxPage.stateDeclined', + }), + }; + } else if (txIsAccepted(tx)) { + return { + nameClassName: css.nameNotEmphasized, + bookingClassName: css.bookingNoActionNeeded, + lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized, + stateClassName: css.stateSucces, + state: intl.formatMessage({ + id: 'InboxPage.stateAccepted', + }), + }; + } else if (txIsCanceled(tx)) { + return { + nameClassName: css.nameNotEmphasized, + bookingClassName: css.bookingNoActionNeeded, + lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized, + stateClassName: css.stateNoActionNeeded, + state: intl.formatMessage({ + id: 'InboxPage.stateCanceled', + }), + }; + } else if (txHasBeenDelivered(tx)) { + return { + nameClassName: css.nameNotEmphasized, + bookingClassName: css.bookingNoActionNeeded, + lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized, + stateClassName: css.stateNoActionNeeded, + state: intl.formatMessage({ + id: 'InboxPage.stateDelivered', + }), + }; + } else { + console.warn('This transition is unknown:', tx.attributes.lastTransition); + return null; } - return { - nameClassName: isOrder ? css.nameRequested : css.namePending, - bookingClassName: isOrder ? css.bookingRequested : css.bookingPending, - lastTransitionedAtClassName: isOrder - ? css.lastTransitionedAtRequested - : css.lastTransitionedAtPending, - stateClassName: isOrder ? css.stateRequested : css.statePending, - state: isOrder - ? intl.formatMessage({ - id: 'InboxPage.stateRequested', - }) - : intl.formatMessage({ - id: 'InboxPage.statePending', - }), - }; }; const bookingData = (unitType, tx, isOrder, intl) => { @@ -174,7 +184,7 @@ BookingInfoMaybe.propTypes = { }; export const InboxItem = props => { - const { unitType, type, tx, intl } = props; + const { unitType, type, tx, intl, stateData } = props; const { customer, provider } = tx; const isOrder = type === 'order'; @@ -185,7 +195,6 @@ export const InboxItem = props => { const isOtherUserBanned = otherUser.attributes.banned; const otherUserDisplayName = userDisplayName(otherUser, bannedUserDisplayName); - const stateData = txState(intl, tx, isOrder); const isSaleNotification = !isOrder && txIsRequested(tx); const rowNotificationDot = isSaleNotification ?
: null; const lastTransitionedAt = formatDate(intl, tx.attributes.lastTransitionedAt); @@ -268,11 +277,15 @@ export const InboxPageComponent = props => { const title = isOrders ? ordersTitle : salesTitle; const toTxItem = tx => { - return ( + const type = isOrders ? 'order' : 'sale'; + const stateData = txState(intl, tx, type); + + // Render InboxItem only if the latest transition of the transaction is handled in the `txState` function. + return stateData ? (
  • - +
  • - ); + ) : null; }; const error = fetchOrdersOrSalesError ? ( diff --git a/src/containers/InboxPage/InboxPage.test.js b/src/containers/InboxPage/InboxPage.test.js index 4bf71a01..f3eef444 100644 --- a/src/containers/InboxPage/InboxPage.test.js +++ b/src/containers/InboxPage/InboxPage.test.js @@ -8,7 +8,7 @@ import { createTransaction, createBooking, } from '../../util/test-data'; -import { InboxPageComponent, InboxItem } from './InboxPage'; +import { InboxPageComponent, InboxItem, txState } from './InboxPage'; import routeConfiguration from '../../routeConfiguration'; import { TRANSITION_REQUEST } from '../../util/transaction'; import { LINE_ITEM_NIGHT } from '../../util/types'; @@ -74,6 +74,8 @@ describe('InboxPage', () => { const ordersTree = renderShallow(); expect(ordersTree).toMatchSnapshot(); + const stateDataOrder = txState(fakeIntl, ordersProps.transactions[0], 'order'); + // Deeply render one InboxItem const orderItem = renderDeep( { type="order" tx={ordersProps.transactions[0]} intl={fakeIntl} + stateData={stateDataOrder} /> ); expect(orderItem).toMatchSnapshot(); @@ -128,6 +131,8 @@ describe('InboxPage', () => { const salesTree = renderShallow(); expect(salesTree).toMatchSnapshot(); + const stateDataSale = txState(fakeIntl, salesProps.transactions[0], 'sale'); + // Deeply render one InboxItem const saleItem = renderDeep( { type="sale" tx={salesProps.transactions[0]} intl={fakeIntl} + stateData={stateDataSale} /> ); expect(saleItem).toMatchSnapshot(); diff --git a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap index 914c89a7..c1b3ae9c 100644 --- a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap +++ b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap @@ -91,6 +91,15 @@ exports[`InboxPage matches snapshot 1`] = ` "now": [Function], } } + stateData={ + Object { + "bookingClassName": undefined, + "lastTransitionedAtClassName": undefined, + "nameClassName": undefined, + "state": "InboxPage.stateRequested", + "stateClassName": undefined, + } + } tx={ Object { "attributes": Object { @@ -216,6 +225,15 @@ exports[`InboxPage matches snapshot 1`] = ` "now": [Function], } } + stateData={ + Object { + "bookingClassName": undefined, + "lastTransitionedAtClassName": undefined, + "nameClassName": undefined, + "state": "InboxPage.stateRequested", + "stateClassName": undefined, + } + } tx={ Object { "attributes": Object { @@ -483,6 +501,15 @@ exports[`InboxPage matches snapshot 3`] = ` "now": [Function], } } + stateData={ + Object { + "bookingClassName": undefined, + "lastTransitionedAtClassName": undefined, + "nameClassName": undefined, + "state": "InboxPage.statePending", + "stateClassName": undefined, + } + } tx={ Object { "attributes": Object { @@ -608,6 +635,15 @@ exports[`InboxPage matches snapshot 3`] = ` "now": [Function], } } + stateData={ + Object { + "bookingClassName": undefined, + "lastTransitionedAtClassName": undefined, + "nameClassName": undefined, + "state": "InboxPage.statePending", + "stateClassName": undefined, + } + } tx={ Object { "attributes": Object { From caad06353e5d1c036982c2f0a43bf9195e407d53 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 28 Jan 2019 18:43:49 +0200 Subject: [PATCH 08/12] Add state graph description and refactor transaction.js functions to use it. --- src/components/ActivityFeed/ActivityFeed.js | 6 +- .../BookingBreakdown/LineItemTotalPrice.js | 6 +- .../TransactionPanel/TransactionPanel.css | 8 + .../TransactionPanel/TransactionPanel.js | 4 +- src/containers/InboxPage/InboxPage.js | 4 +- .../TransactionPage/TransactionPage.duck.js | 4 +- src/util/transaction.js | 304 +++++++++++++----- src/util/transaction.test.js | 96 ++++++ 8 files changed, 337 insertions(+), 95 deletions(-) create mode 100644 src/util/transaction.test.js diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js index e92f7dad..8087e9a2 100644 --- a/src/components/ActivityFeed/ActivityFeed.js +++ b/src/components/ActivityFeed/ActivityFeed.js @@ -20,7 +20,7 @@ import { TRANSITION_REVIEW_2_BY_PROVIDER, TX_TRANSITION_ACTOR_CUSTOMER, TX_TRANSITION_ACTOR_PROVIDER, - areReviewsCompleted, + transitionIsReviewed, } from '../../util/transaction'; import { propTypes } from '../../util/types'; import * as log from '../../util/log'; @@ -122,7 +122,7 @@ const hasUserLeftAReviewFirst = (userRole, lastTransition) => { userRole === TX_TRANSITION_ACTOR_CUSTOMER) || (lastTransition === TRANSITION_REVIEW_1_BY_PROVIDER && userRole === TX_TRANSITION_ACTOR_PROVIDER) || - areReviewsCompleted(lastTransition) + transitionIsReviewed(lastTransition) ); }; @@ -267,7 +267,7 @@ const Transition = props => { let reviewComponent = null; - if (isReviewTransition(currentTransition) && areReviewsCompleted(lastTransition)) { + if (isReviewTransition(currentTransition) && transitionIsReviewed(lastTransition)) { const customerReview = currentTransition === TRANSITION_REVIEW_1_BY_CUSTOMER || currentTransition === TRANSITION_REVIEW_2_BY_CUSTOMER; diff --git a/src/components/BookingBreakdown/LineItemTotalPrice.js b/src/components/BookingBreakdown/LineItemTotalPrice.js index 92099050..c55a036d 100644 --- a/src/components/BookingBreakdown/LineItemTotalPrice.js +++ b/src/components/BookingBreakdown/LineItemTotalPrice.js @@ -2,7 +2,7 @@ import React from 'react'; import { bool } from 'prop-types'; import { FormattedMessage, intlShape } from 'react-intl'; import { formatMoney } from '../../util/currency'; -import { txIsCanceled, txIsCompleted, txIsDeclinedOrExpired } from '../../util/transaction'; +import { txIsCanceled, txIsDelivered, txIsDeclined } from '../../util/transaction'; import { propTypes } from '../../util/types'; import css from './BookingBreakdown.css'; @@ -11,9 +11,9 @@ const LineItemUnitPrice = props => { const { transaction, isProvider, intl } = props; let providerTotalMessageId = 'BookingBreakdown.providerTotalDefault'; - if (txIsCompleted(transaction)) { + if (txIsDelivered(transaction)) { providerTotalMessageId = 'BookingBreakdown.providerTotalDelivered'; - } else if (txIsDeclinedOrExpired(transaction)) { + } else if (txIsDeclined(transaction)) { providerTotalMessageId = 'BookingBreakdown.providerTotalDeclined'; } else if (txIsCanceled(transaction)) { providerTotalMessageId = 'BookingBreakdown.providerTotalCanceled'; diff --git a/src/components/TransactionPanel/TransactionPanel.css b/src/components/TransactionPanel/TransactionPanel.css index 3c71b6fa..6403385a 100644 --- a/src/components/TransactionPanel/TransactionPanel.css +++ b/src/components/TransactionPanel/TransactionPanel.css @@ -285,6 +285,14 @@ } } +.breakdownContainer { + display: none; + + @media (--viewportLarge) { + display: block; + } +} + /* FeedSection subcomponent */ .feedHeading { color: var(--matterColorAnti); diff --git a/src/components/TransactionPanel/TransactionPanel.js b/src/components/TransactionPanel/TransactionPanel.js index b402bcc3..b59b0ad5 100644 --- a/src/components/TransactionPanel/TransactionPanel.js +++ b/src/components/TransactionPanel/TransactionPanel.js @@ -5,7 +5,7 @@ import classNames from 'classnames'; import { txIsAccepted, txIsCanceled, - txIsDeclinedOrExpired, + txIsDeclined, txIsEnquired, txIsRequested, txHasBeenDelivered, @@ -203,7 +203,7 @@ export class TransactionPanelComponent extends Component { showDetailCardHeadings: isCustomer, showAddress: isCustomer, }; - } else if (txIsDeclinedOrExpired(tx)) { + } else if (txIsDeclined(tx)) { return { headingState: HEADING_DECLINED, showDetailCardHeadings: isCustomer, diff --git a/src/containers/InboxPage/InboxPage.js b/src/containers/InboxPage/InboxPage.js index f0e95c16..6d5544ab 100644 --- a/src/containers/InboxPage/InboxPage.js +++ b/src/containers/InboxPage/InboxPage.js @@ -8,7 +8,7 @@ import classNames from 'classnames'; import { txIsAccepted, txIsCanceled, - txIsDeclinedOrExpired, + txIsDeclined, txIsEnquired, txIsRequested, txHasBeenDelivered, @@ -88,7 +88,7 @@ export const txState = (intl, tx, type) => { }; return requested; - } else if (txIsDeclinedOrExpired(tx)) { + } else if (txIsDeclined(tx)) { return { nameClassName: css.nameNotEmphasized, bookingClassName: css.bookingNoActionNeeded, diff --git a/src/containers/TransactionPage/TransactionPage.duck.js b/src/containers/TransactionPage/TransactionPage.duck.js index ac59f84e..903777bf 100644 --- a/src/containers/TransactionPage/TransactionPage.duck.js +++ b/src/containers/TransactionPage/TransactionPage.duck.js @@ -9,7 +9,7 @@ import { txIsEnquired, getReview1Transition, getReview2Transition, - txHasFirstReviewBy, + txIsInFirstReviewBy, TRANSITION_ACCEPT, TRANSITION_DECLINE, } from '../../util/transaction'; @@ -495,7 +495,7 @@ const sendReviewAsFirst = (id, params, role, dispatch, sdk) => { export const sendReview = (role, tx, reviewRating, reviewContent) => (dispatch, getState, sdk) => { const params = { reviewRating, reviewContent }; - const txStateOtherPartyFirst = txHasFirstReviewBy(tx, role !== CUSTOMER); + const txStateOtherPartyFirst = txIsInFirstReviewBy(tx, role !== CUSTOMER); dispatch(sendReviewRequest()); diff --git a/src/util/transaction.js b/src/util/transaction.js index 42e6c3ee..694b209c 100644 --- a/src/util/transaction.js +++ b/src/util/transaction.js @@ -1,5 +1,14 @@ import { ensureTransaction } from './data'; +/** + * Transitions + * + * These strings must sync with values defined in Flex API, + * since transaction objects given by API contain info about last transitions. + * All the actions in API side happen in transitions, + * so we need to understand what those strings mean. + */ + // When a customer makes a booking to a listing, a transaction is // created with the initial request transition. export const TRANSITION_REQUEST = 'transition/request'; @@ -34,89 +43,11 @@ export const TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD = 'transition/expire-custo export const TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD = 'transition/expire-provider-review-period'; export const TRANSITION_EXPIRE_REVIEW_PERIOD = 'transition/expire-review-period'; -export const TRANSITIONS = [ - TRANSITION_ACCEPT, - TRANSITION_CANCEL, - TRANSITION_COMPLETE, - TRANSITION_DECLINE, - TRANSITION_ENQUIRE, - TRANSITION_EXPIRE, - TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, - TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, - TRANSITION_EXPIRE_REVIEW_PERIOD, - TRANSITION_REQUEST, - TRANSITION_REQUEST_AFTER_ENQUIRY, - TRANSITION_REVIEW_1_BY_CUSTOMER, - TRANSITION_REVIEW_1_BY_PROVIDER, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, -]; - -const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition; - -export const txIsEnquired = tx => txLastTransition(tx) === TRANSITION_ENQUIRE; - -export const transitionsToRequested = [TRANSITION_REQUEST, TRANSITION_REQUEST_AFTER_ENQUIRY]; -export const txIsRequested = tx => transitionsToRequested.includes(txLastTransition(tx)); - -export const txIsAccepted = tx => txLastTransition(tx) === TRANSITION_ACCEPT; - -export const txIsDeclined = tx => txLastTransition(tx) === TRANSITION_DECLINE; - -export const txIsExpired = tx => txLastTransition(tx) === TRANSITION_EXPIRE; - -export const txIsDeclinedOrExpired = tx => txIsDeclined(tx) || txIsExpired(tx); - -export const txIsCanceled = tx => txLastTransition(tx) === TRANSITION_CANCEL; - -export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE; - -const firstReviewTransitions = [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_1_BY_PROVIDER]; -export const txHasFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx)); - -export const txHasFirstReviewBy = (tx, isCustomer) => - isCustomer - ? txLastTransition(tx) === TRANSITION_REVIEW_1_BY_CUSTOMER - : txLastTransition(tx) === TRANSITION_REVIEW_1_BY_PROVIDER; - -export const txIsReviewed = tx => areReviewsCompleted(txLastTransition(tx)); - -export const getReview1Transition = isCustomer => - isCustomer ? TRANSITION_REVIEW_1_BY_CUSTOMER : TRANSITION_REVIEW_1_BY_PROVIDER; - -export const getReview2Transition = isCustomer => - isCustomer ? TRANSITION_REVIEW_2_BY_CUSTOMER : TRANSITION_REVIEW_2_BY_PROVIDER; - -// Check if tx transition is followed by a state where -// reviews are completed -export const areReviewsCompleted = transition => { - return [ - TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, - TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, - TRANSITION_EXPIRE_REVIEW_PERIOD, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, - ].includes(transition); -}; - -export const txHasBeenAccepted = tx => { - const transition = txLastTransition(tx); - return [ - TRANSITION_ACCEPT, - TRANSITION_REVIEW_1_BY_CUSTOMER, - TRANSITION_REVIEW_1_BY_PROVIDER, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, - TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD, - TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD, - TRANSITION_EXPIRE_REVIEW_PERIOD, - TRANSITION_COMPLETE, - ].includes(transition); -}; - -export const txHasBeenDelivered = tx => { - return txHasFirstReview(tx) || txIsReviewed(tx) || txIsCompleted(tx); -}; +/** + * Actors + * + * There are 4 different actors that might initiate transitions: + */ // Roles of actors that perform transaction transitions export const TX_TRANSITION_ACTOR_CUSTOMER = 'customer'; @@ -130,3 +61,210 @@ export const TX_TRANSITION_ACTORS = [ TX_TRANSITION_ACTOR_SYSTEM, TX_TRANSITION_ACTOR_OPERATOR, ]; + +/** + * States + * + * These constants are only for making it clear how transitions work together. + * You should not use these constants outside of this file. + * + * Note: these states are not in sync with states used transaction process definitions + * in Marketplace API. Only last transitions are passed along transaction object. + */ +const STATE_INITIAL = 'initial'; +const STATE_ENQUIRY = 'enquiry'; +const STATE_PREAUTHORIZED = 'preauthorized'; +const STATE_DECLINED = 'declined'; +const STATE_ACCEPTED = 'accepted'; +const STATE_CANCELED = 'canceled'; +const STATE_DELIVERED = 'delivered'; +const STATE_REVIEWED = 'reviewed'; +const STATE_REVIEWED_BY_CUSTOMER = 'reviewed-by-customer'; +const STATE_REVIEWED_BY_PROVIDER = 'reviewed-by-provider'; + +/** + * Description of transaction process + * + * You should keep this in sync with transaction process defined in Marketplace API + * + * Note: we don't use yet any state machine library, + * but this description format is following Xstate (FSM library) + * https://xstate.js.org/docs/ + */ +const stateDescription = { + // id is defined only to support Xstate format. + // However if you have multiple transaction processes defined, + // it is best to keep them in sync with transaction process aliases. + id: 'preauth-with-nightly-booking/release-1', + + // This 'initial' state is a starting point for new transaction + initial: STATE_INITIAL, + + // States + states: { + [STATE_INITIAL]: { + on: { + [TRANSITION_ENQUIRE]: STATE_ENQUIRY, + [TRANSITION_REQUEST]: STATE_PREAUTHORIZED, + }, + }, + [STATE_ENQUIRY]: { + on: { + [TRANSITION_REQUEST_AFTER_ENQUIRY]: STATE_PREAUTHORIZED, + }, + }, + + [STATE_PREAUTHORIZED]: { + on: { + [TRANSITION_DECLINE]: STATE_DECLINED, + [TRANSITION_EXPIRE]: STATE_DECLINED, + [TRANSITION_ACCEPT]: STATE_ACCEPTED, + }, + }, + + [STATE_DECLINED]: {}, + [STATE_ACCEPTED]: { + on: { + [TRANSITION_CANCEL]: STATE_CANCELED, + [TRANSITION_COMPLETE]: STATE_DELIVERED, + }, + }, + + [STATE_CANCELED]: {}, + [STATE_DELIVERED]: { + on: { + [TRANSITION_EXPIRE_REVIEW_PERIOD]: STATE_REVIEWED, + [TRANSITION_REVIEW_1_BY_CUSTOMER]: STATE_REVIEWED_BY_CUSTOMER, + [TRANSITION_REVIEW_1_BY_PROVIDER]: STATE_REVIEWED_BY_PROVIDER, + }, + }, + + [STATE_REVIEWED_BY_CUSTOMER]: { + on: { + [TRANSITION_REVIEW_2_BY_PROVIDER]: STATE_REVIEWED, + [TRANSITION_EXPIRE_PROVIDER_REVIEW_PERIOD]: STATE_REVIEWED, + }, + }, + [STATE_REVIEWED_BY_PROVIDER]: { + on: { + [TRANSITION_REVIEW_2_BY_CUSTOMER]: STATE_REVIEWED, + [TRANSITION_EXPIRE_CUSTOMER_REVIEW_PERIOD]: STATE_REVIEWED, + }, + }, + [STATE_REVIEWED]: { type: 'final' }, + }, +}; + +// Note: currently we assume that state description doesn't contain nested states. +const statesFromStateDescription = description => description.states || {}; + +// Get all the transitions from states object in an array +const getTransitions = states => { + const stateNames = Object.keys(states); + + const transitionsReducer = (transitionArray, name) => { + const stateTransitions = states[name] && states[name].on; + const transitionKeys = stateTransitions ? Object.keys(stateTransitions) : []; + return [ + ...transitionArray, + ...transitionKeys.map(key => ({ key, value: stateTransitions[key] })), + ]; + }; + + return stateNames.reduce(transitionsReducer, []); +}; + +// This is a list of all the transitions that this app should be able to handle. +export const TRANSITIONS = getTransitions(statesFromStateDescription(stateDescription)).map( + t => t.key +); + +// This function returns a function that has given stateDesc in scope chain. +const getTransitionsToStateFn = stateDesc => state => + getTransitions(statesFromStateDescription(stateDesc)) + .filter(t => t.value === state) + .map(t => t.key); + +// Get all the transitions that lead to specified state. +const getTransitionsToState = getTransitionsToStateFn(stateDescription); + +// This is needed to fetch transactions that need response from provider. +// I.e. transactions which provider needs to accept or decline +export const transitionsToRequested = getTransitionsToState(STATE_PREAUTHORIZED); + +/** + * Helper functions to figure out if transaction is in a specific state. + * State is based on lastTransition given by transaction object and state description. + */ + +const txLastTransition = tx => ensureTransaction(tx).attributes.lastTransition; + +// DEPRECATED: use txIsDelivered instead +export const txIsCompleted = tx => txLastTransition(tx) === TRANSITION_COMPLETE; + +export const txIsEnquired = tx => + getTransitionsToState(STATE_ENQUIRY).includes(txLastTransition(tx)); + +// Note: state name used in Marketplace API docs (and here) is actually preauthorized +// However, word "requested" is used in many places so that we decided to keep it. +export const txIsRequested = tx => + getTransitionsToState(STATE_PREAUTHORIZED).includes(txLastTransition(tx)); + +export const txIsAccepted = tx => + getTransitionsToState(STATE_ACCEPTED).includes(txLastTransition(tx)); + +export const txIsDeclined = tx => + getTransitionsToState(STATE_DECLINED).includes(txLastTransition(tx)); + +export const txIsCanceled = tx => + getTransitionsToState(STATE_CANCELED).includes(txLastTransition(tx)); + +export const txIsDelivered = tx => + getTransitionsToState(STATE_DELIVERED).includes(txLastTransition(tx)); + +const firstReviewTransitions = [ + ...getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER), + ...getTransitionsToState(STATE_REVIEWED_BY_PROVIDER), +]; +export const txIsInFirstReview = tx => firstReviewTransitions.includes(txLastTransition(tx)); + +export const txIsInFirstReviewBy = (tx, isCustomer) => + isCustomer + ? getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER).includes(txLastTransition(tx)) + : getTransitionsToState(STATE_REVIEWED_BY_PROVIDER).includes(txLastTransition(tx)); + +export const txIsReviewed = tx => + getTransitionsToState(STATE_REVIEWED).includes(txLastTransition(tx)); + +/** + * Helper functions to figure out if transaction has passed a given state. + * This is based on transitions history given by transaction object. + */ + +const txTransitions = tx => ensureTransaction(tx).attributes.transitions || []; +const hasPassedTransition = (transitionName, tx) => + !!txTransitions(tx).find(t => t.transition === transitionName); + +const hasPassedStateFn = state => tx => + getTransitionsToState(state).filter(t => hasPassedTransition(t, tx)).length > 0; + +export const txHasBeenAccepted = hasPassedStateFn(STATE_ACCEPTED); +export const txHasBeenDelivered = hasPassedStateFn(STATE_DELIVERED); + +/** + * Other transaction related utility functions + */ + +export const transitionIsReviewed = transition => + getTransitionsToState(STATE_REVIEWED).includes(transition); + +export const transitionIsFirstReviewedBy = (transition, isCustomer) => + isCustomer + ? getTransitionsToState(STATE_REVIEWED_BY_CUSTOMER).includes(transition) + : getTransitionsToState(STATE_REVIEWED_BY_PROVIDER).includes(transition); + +export const getReview1Transition = isCustomer => + isCustomer ? TRANSITION_REVIEW_1_BY_CUSTOMER : TRANSITION_REVIEW_1_BY_PROVIDER; + +export const getReview2Transition = isCustomer => + isCustomer ? TRANSITION_REVIEW_2_BY_CUSTOMER : TRANSITION_REVIEW_2_BY_PROVIDER; diff --git a/src/util/transaction.test.js b/src/util/transaction.test.js new file mode 100644 index 00000000..a48519c0 --- /dev/null +++ b/src/util/transaction.test.js @@ -0,0 +1,96 @@ +import { createUser, createTransaction, createListing, createTxTransition } from './test-data'; + +import { + TX_TRANSITION_ACTOR_CUSTOMER, + TX_TRANSITION_ACTOR_PROVIDER, + TX_TRANSITION_ACTOR_SYSTEM, + TRANSITION_REQUEST, + TRANSITION_ACCEPT, + TRANSITION_COMPLETE, + TRANSITION_EXPIRE_REVIEW_PERIOD, + txIsAccepted, + txIsReviewed, + txHasBeenAccepted, + txHasBeenDelivered, +} from './transaction'; + +const transitionRequest = createTxTransition({ + createdAt: new Date(Date.UTC(2017, 10, 9, 8, 10)), + by: TX_TRANSITION_ACTOR_CUSTOMER, + transition: TRANSITION_REQUEST, +}); + +const transitionAccept = createTxTransition({ + createdAt: new Date(Date.UTC(2017, 10, 9, 8, 12)), + by: TX_TRANSITION_ACTOR_PROVIDER, + transition: TRANSITION_ACCEPT, +}); + +const transitionDelivered = createTxTransition({ + createdAt: new Date(Date.UTC(2017, 10, 16, 8, 12)), + by: TX_TRANSITION_ACTOR_SYSTEM, + transition: TRANSITION_COMPLETE, +}); +const transitionReviewed = createTxTransition({ + createdAt: new Date(Date.UTC(2017, 10, 16, 8, 12)), + by: TX_TRANSITION_ACTOR_SYSTEM, + transition: TRANSITION_EXPIRE_REVIEW_PERIOD, +}); + +const txRequested = createTransaction({ + lastTransition: TRANSITION_REQUEST, + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + transitions: [transitionRequest], +}); + +const txAccepted = createTransaction({ + lastTransition: TRANSITION_ACCEPT, + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + transitions: [transitionRequest, transitionAccept], +}); + +const txReviewed = createTransaction({ + lastTransition: TRANSITION_EXPIRE_REVIEW_PERIOD, + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + transitions: [transitionRequest, transitionAccept, transitionDelivered, transitionReviewed], +}); + +describe('transaction utils', () => { + describe('tx is in correct state', () => { + it(`txIsReviewed(txReviewed) succeeds with last transaction: ${TRANSITION_EXPIRE_REVIEW_PERIOD}`, () => { + expect(txIsReviewed(txReviewed)).toEqual(true); + }); + it(`txIsAccepted(txReviewed) fails with last transaction: ${TRANSITION_EXPIRE_REVIEW_PERIOD}`, () => { + expect(txIsAccepted(txReviewed)).toEqual(false); + }); + }); + + describe('tx has passed a state X', () => { + it('txHasBeenAccepted(txRequested) fails', () => { + expect(txHasBeenAccepted(txRequested)).toEqual(false); + }); + it('txHasBeenDelivered(txRequest) fails', () => { + expect(txHasBeenDelivered(txRequested)).toEqual(false); + }); + + it('txHasBeenAccepted(txAccepted) succeeds', () => { + expect(txHasBeenAccepted(txAccepted)).toEqual(true); + }); + it('txHasBeenDelivered(txAccepted) fails', () => { + expect(txHasBeenDelivered(txAccepted)).toEqual(false); + }); + + it('txHasBeenAccepted(txReviewed) succeeds', () => { + expect(txHasBeenAccepted(txReviewed)).toEqual(true); + }); + it('txHasBeenDelivered(txReviewed) succeeds', () => { + expect(txHasBeenDelivered(txReviewed)).toEqual(true); + }); + }); +}); From 726389c941b3544bf928f6e96b98be380a3ede8a Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 28 Jan 2019 18:44:30 +0200 Subject: [PATCH 09/12] Update tests in TransactionPanel. --- .../TransactionPanel/TransactionPanel.test.js | 35 +++++++++++++++++++ .../TransactionPanel.test.js.snap | 30 ++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/components/TransactionPanel/TransactionPanel.test.js b/src/components/TransactionPanel/TransactionPanel.test.js index 58e83941..8f6b299a 100644 --- a/src/components/TransactionPanel/TransactionPanel.test.js +++ b/src/components/TransactionPanel/TransactionPanel.test.js @@ -2,6 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { types as sdkTypes } from '../../util/sdkLoader'; import { + createTxTransition, createTransaction, createBooking, createListing, @@ -82,6 +83,23 @@ describe('TransactionPanel - Sale', () => { const txDelivered = createTransaction({ id: 'sale-delivered', lastTransition: TRANSITION_COMPLETE, + transitions: [ + createTxTransition({ + createdAt: new Date(Date.UTC(2017, 4, 1)), + by: 'customer', + transition: TRANSITION_REQUEST, + }), + createTxTransition({ + createdAt: new Date(Date.UTC(2017, 5, 1)), + by: 'provider', + transition: TRANSITION_ACCEPT, + }), + createTxTransition({ + createdAt: new Date(Date.UTC(2017, 6, 1)), + by: 'system', + transition: TRANSITION_COMPLETE, + }), + ], ...baseTxAttrs, }); @@ -250,6 +268,23 @@ describe('TransactionPanel - Order', () => { const txDelivered = createTransaction({ id: 'order-delivered', lastTransition: TRANSITION_COMPLETE, + transitions: [ + createTxTransition({ + createdAt: new Date(Date.UTC(2017, 4, 1)), + by: 'customer', + transition: TRANSITION_REQUEST, + }), + createTxTransition({ + createdAt: new Date(Date.UTC(2017, 5, 1)), + by: 'provider', + transition: TRANSITION_ACCEPT, + }), + createTxTransition({ + createdAt: new Date(Date.UTC(2017, 6, 1)), + by: 'system', + transition: TRANSITION_COMPLETE, + }), + ], ...baseTxAttrs, }); diff --git a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap index aaca233c..677dc392 100644 --- a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap +++ b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap @@ -2386,6 +2386,11 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = ` "createdAt": 2017-06-01T00:00:00.000Z, "transition": "transition/accept", }, + Object { + "by": "system", + "createdAt": 2017-07-01T00:00:00.000Z, + "transition": "transition/complete", + }, ], }, "booking": Object { @@ -2514,6 +2519,11 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = ` "createdAt": 2017-06-01T00:00:00.000Z, "transition": "transition/accept", }, + Object { + "by": "system", + "createdAt": 2017-07-01T00:00:00.000Z, + "transition": "transition/complete", + }, ], }, "booking": Object { @@ -2767,6 +2777,11 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = ` "createdAt": 2017-06-01T00:00:00.000Z, "transition": "transition/accept", }, + Object { + "by": "system", + "createdAt": 2017-07-01T00:00:00.000Z, + "transition": "transition/complete", + }, ], }, "booking": Object { @@ -6374,6 +6389,11 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = ` "createdAt": 2017-06-01T00:00:00.000Z, "transition": "transition/accept", }, + Object { + "by": "system", + "createdAt": 2017-07-01T00:00:00.000Z, + "transition": "transition/complete", + }, ], }, "booking": Object { @@ -6502,6 +6522,11 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = ` "createdAt": 2017-06-01T00:00:00.000Z, "transition": "transition/accept", }, + Object { + "by": "system", + "createdAt": 2017-07-01T00:00:00.000Z, + "transition": "transition/complete", + }, ], }, "booking": Object { @@ -6755,6 +6780,11 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = ` "createdAt": 2017-06-01T00:00:00.000Z, "transition": "transition/accept", }, + Object { + "by": "system", + "createdAt": 2017-07-01T00:00:00.000Z, + "transition": "transition/complete", + }, ], }, "booking": Object { From f824e4fd2ba1ac4809861cf6e32a055f97731d07 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Thu, 24 Jan 2019 16:08:06 +0200 Subject: [PATCH 10/12] Refactor ActivityFeed component Add function for checking ownRole Use txHasReviewBy function add isCustomerReview and isProviderReview functions --- src/components/ActivityFeed/ActivityFeed.js | 46 ++++++++++----------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js index 8087e9a2..8c6eb30e 100644 --- a/src/components/ActivityFeed/ActivityFeed.js +++ b/src/components/ActivityFeed/ActivityFeed.js @@ -21,12 +21,21 @@ import { TX_TRANSITION_ACTOR_CUSTOMER, TX_TRANSITION_ACTOR_PROVIDER, transitionIsReviewed, + txIsInFirstReviewBy, } from '../../util/transaction'; import { propTypes } from '../../util/types'; import * as log from '../../util/log'; import css from './ActivityFeed.css'; +const isCustomerReview = transition => { + return [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_2_BY_CUSTOMER].includes(transition); +}; + +const isProviderReview = transition => { + return [TRANSITION_REVIEW_1_BY_PROVIDER, TRANSITION_REVIEW_2_BY_PROVIDER].includes(transition); +}; +const txRoleIsProvider = userRole => userRole === TX_TRANSITION_ACTOR_PROVIDER; const Message = props => { const { message, intl } = props; const todayString = intl.formatMessage({ id: 'ActivityFeed.today' }); @@ -116,19 +125,18 @@ const isReviewTransition = transition => { ].includes(transition); }; -const hasUserLeftAReviewFirst = (userRole, lastTransition) => { +const hasUserLeftAReviewFirst = (userRole, transaction, lastTransition) => { + const isProvider = txRoleIsProvider(userRole); return ( - (lastTransition === TRANSITION_REVIEW_1_BY_CUSTOMER && - userRole === TX_TRANSITION_ACTOR_CUSTOMER) || - (lastTransition === TRANSITION_REVIEW_1_BY_PROVIDER && - userRole === TX_TRANSITION_ACTOR_PROVIDER) || + (isProvider && txIsInFirstReviewBy(transaction, isProvider)) || + (!isProvider && txIsInFirstReviewBy(transaction, !isProvider)) || transitionIsReviewed(lastTransition) ); }; const resolveTransitionMessage = ( + transaction, transition, - lastTransition, listingTitle, ownRole, otherUsersName, @@ -138,7 +146,6 @@ const resolveTransitionMessage = ( const isOwnTransition = transition.by === ownRole; const currentTransition = transition.transition; const displayName = otherUsersName; - const deliveredState = lastTransition === TRANSITION_COMPLETE; switch (currentTransition) { case TRANSITION_REQUEST: @@ -164,7 +171,7 @@ const resolveTransitionMessage = ( ); case TRANSITION_EXPIRE: - return ownRole === TX_TRANSITION_ACTOR_PROVIDER ? ( + return txRoleIsProvider(ownRole) ? ( ) : ( @@ -175,7 +182,7 @@ const resolveTransitionMessage = ( // Show the leave a review link if the state is delivered or // if current user is not the first to leave a review const reviewLink = - deliveredState || !hasUserLeftAReviewFirst(ownRole, lastTransition) ? ( + deliveredState || !hasUserLeftAReviewFirst(ownRole, transaction, lastTransition) ? ( @@ -189,7 +196,7 @@ const resolveTransitionMessage = ( } else { // show the leave a review link if current user is not the first // one to leave a review - const reviewLink = !hasUserLeftAReviewFirst(ownRole, lastTransition) ? ( + const reviewLink = !hasUserLeftAReviewFirst(ownRole, transaction) ? ( @@ -249,14 +256,13 @@ const Transition = props => { const bannedUserDisplayName = intl.formatMessage({ id: 'ActivityFeed.bannedUserDisplayName', }); - const otherUsersName = - ownRole === TX_TRANSITION_ACTOR_CUSTOMER - ? userDisplayName(provider, bannedUserDisplayName) - : userDisplayName(customer, bannedUserDisplayName); + const otherUsersName = txRoleIsProvider(ownRole) + ? userDisplayName(customer, bannedUserDisplayName) + : userDisplayName(provider, bannedUserDisplayName); const transitionMessage = resolveTransitionMessage( + transaction, transition, - lastTransition, listingTitle, ownRole, otherUsersName, @@ -268,18 +274,12 @@ const Transition = props => { let reviewComponent = null; if (isReviewTransition(currentTransition) && transitionIsReviewed(lastTransition)) { - const customerReview = - currentTransition === TRANSITION_REVIEW_1_BY_CUSTOMER || - currentTransition === TRANSITION_REVIEW_2_BY_CUSTOMER; - const providerReview = - currentTransition === TRANSITION_REVIEW_1_BY_PROVIDER || - currentTransition === TRANSITION_REVIEW_2_BY_PROVIDER; - if (customerReview) { + if (isCustomerReview(currentTransition)) { const review = reviewByAuthorId(currentTransaction, customer.id); reviewComponent = ( ); - } else if (providerReview) { + } else if (isProviderReview(currentTransition)) { const review = reviewByAuthorId(currentTransaction, provider.id); reviewComponent = ( From f340c19ae812520e26177ace8695ce48f9d18d67 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Thu, 24 Jan 2019 16:31:16 +0200 Subject: [PATCH 11/12] Use generic functions from transaction.js (and move some generic in-file functions there). --- src/components/ActivityFeed/ActivityFeed.js | 76 +++++++-------------- src/util/transaction.js | 44 ++++++++++++ 2 files changed, 67 insertions(+), 53 deletions(-) diff --git a/src/components/ActivityFeed/ActivityFeed.js b/src/components/ActivityFeed/ActivityFeed.js index 8c6eb30e..534e79ec 100644 --- a/src/components/ActivityFeed/ActivityFeed.js +++ b/src/components/ActivityFeed/ActivityFeed.js @@ -18,24 +18,21 @@ import { TRANSITION_REVIEW_1_BY_PROVIDER, TRANSITION_REVIEW_2_BY_CUSTOMER, TRANSITION_REVIEW_2_BY_PROVIDER, - TX_TRANSITION_ACTOR_CUSTOMER, - TX_TRANSITION_ACTOR_PROVIDER, transitionIsReviewed, + txIsDelivered, txIsInFirstReviewBy, + txIsReviewed, + isCustomerReview, + isProviderReview, + txRoleIsProvider, + getUserTxRole, + isRelevantPastTransition, } from '../../util/transaction'; import { propTypes } from '../../util/types'; import * as log from '../../util/log'; import css from './ActivityFeed.css'; -const isCustomerReview = transition => { - return [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_2_BY_CUSTOMER].includes(transition); -}; - -const isProviderReview = transition => { - return [TRANSITION_REVIEW_1_BY_PROVIDER, TRANSITION_REVIEW_2_BY_PROVIDER].includes(transition); -}; -const txRoleIsProvider = userRole => userRole === TX_TRANSITION_ACTOR_PROVIDER; const Message = props => { const { message, intl } = props; const todayString = intl.formatMessage({ id: 'ActivityFeed.today' }); @@ -96,42 +93,11 @@ Review.propTypes = { rating: number.isRequired, }; -// Check if a transition is the kind that -// should be rendered in he ActivityFeed -const shouldRenderTransition = transition => { - return [ - TRANSITION_ACCEPT, - TRANSITION_CANCEL, - TRANSITION_COMPLETE, - TRANSITION_DECLINE, - TRANSITION_EXPIRE, - TRANSITION_REQUEST, - TRANSITION_REQUEST_AFTER_ENQUIRY, - TRANSITION_REVIEW_1_BY_CUSTOMER, - TRANSITION_REVIEW_1_BY_PROVIDER, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, - ].includes(transition); -}; - -// Check if a user giving a review is related to -// given tx transition. -const isReviewTransition = transition => { - return [ - TRANSITION_REVIEW_1_BY_CUSTOMER, - TRANSITION_REVIEW_1_BY_PROVIDER, - TRANSITION_REVIEW_2_BY_CUSTOMER, - TRANSITION_REVIEW_2_BY_PROVIDER, - ].includes(transition); -}; - -const hasUserLeftAReviewFirst = (userRole, transaction, lastTransition) => { +const hasUserLeftAReviewFirst = (userRole, transaction) => { const isProvider = txRoleIsProvider(userRole); - return ( - (isProvider && txIsInFirstReviewBy(transaction, isProvider)) || - (!isProvider && txIsInFirstReviewBy(transaction, !isProvider)) || - transitionIsReviewed(lastTransition) - ); + return isProvider + ? txIsInFirstReviewBy(transaction, isProvider) + : txIsInFirstReviewBy(transaction, !isProvider); }; const resolveTransitionMessage = ( @@ -181,14 +147,19 @@ const resolveTransitionMessage = ( case TRANSITION_COMPLETE: // Show the leave a review link if the state is delivered or // if current user is not the first to leave a review + const reviewPeriodJustStarted = txIsDelivered(transaction); + const reviewPeriodIsOver = txIsReviewed(transaction); + const userHasLeftAReview = hasUserLeftAReviewFirst(ownRole, transaction); + const reviewLink = - deliveredState || !hasUserLeftAReviewFirst(ownRole, transaction, lastTransition) ? ( + reviewPeriodJustStarted || !(reviewPeriodIsOver || userHasLeftAReview) ? ( ) : null; return ; + case TRANSITION_REVIEW_1_BY_PROVIDER: case TRANSITION_REVIEW_1_BY_CUSTOMER: if (isOwnTransition) { @@ -196,7 +167,9 @@ const resolveTransitionMessage = ( } else { // show the leave a review link if current user is not the first // one to leave a review - const reviewLink = !hasUserLeftAReviewFirst(ownRole, transaction) ? ( + const reviewPeriodIsOver = txIsReviewed(transaction); + const userHasLeftAReview = hasUserLeftAReviewFirst(ownRole, transaction); + const reviewLink = !(reviewPeriodIsOver || userHasLeftAReview) ? ( @@ -248,10 +221,7 @@ const Transition = props => { : currentTransaction.listing.attributes.title; const lastTransition = currentTransaction.attributes.lastTransition; - const ownRole = - currentUser.id.uuid === customer.id.uuid - ? TX_TRANSITION_ACTOR_CUSTOMER - : TX_TRANSITION_ACTOR_PROVIDER; + const ownRole = getUserTxRole(currentUser.id, currentTransaction); const bannedUserDisplayName = intl.formatMessage({ id: 'ActivityFeed.bannedUserDisplayName', @@ -273,7 +243,7 @@ const Transition = props => { let reviewComponent = null; - if (isReviewTransition(currentTransition) && transitionIsReviewed(lastTransition)) { + if (transitionIsReviewed(lastTransition)) { if (isCustomerReview(currentTransition)) { const review = reviewByAuthorId(currentTransaction, customer.id); reviewComponent = ( @@ -417,7 +387,7 @@ export const ActivityFeedComponent = props => { }; const transitionListItem = transition => { - if (shouldRenderTransition(transition.transition)) { + if (isRelevantPastTransition(transition.transition)) { return (
  • {transitionComponent(transition)} diff --git a/src/util/transaction.js b/src/util/transaction.js index 694b209c..bf9207b0 100644 --- a/src/util/transaction.js +++ b/src/util/transaction.js @@ -268,3 +268,47 @@ export const getReview1Transition = isCustomer => export const getReview2Transition = isCustomer => isCustomer ? TRANSITION_REVIEW_2_BY_CUSTOMER : TRANSITION_REVIEW_2_BY_PROVIDER; + +// Check if a transition is the kind that should be rendered +// when showing transition history (e.g. ActivityFeed) +// The first transition and most of the expiration transitions made by system are not relevant +export const isRelevantPastTransition = transition => { + return [ + TRANSITION_ACCEPT, + TRANSITION_CANCEL, + TRANSITION_COMPLETE, + TRANSITION_DECLINE, + TRANSITION_EXPIRE, + TRANSITION_REQUEST, + TRANSITION_REQUEST_AFTER_ENQUIRY, + TRANSITION_REVIEW_1_BY_CUSTOMER, + TRANSITION_REVIEW_1_BY_PROVIDER, + TRANSITION_REVIEW_2_BY_CUSTOMER, + TRANSITION_REVIEW_2_BY_PROVIDER, + ].includes(transition); +}; + +export const isCustomerReview = transition => { + return [TRANSITION_REVIEW_1_BY_CUSTOMER, TRANSITION_REVIEW_2_BY_CUSTOMER].includes(transition); +}; + +export const isProviderReview = transition => { + return [TRANSITION_REVIEW_1_BY_PROVIDER, TRANSITION_REVIEW_2_BY_PROVIDER].includes(transition); +}; + +export const getUserTxRole = (currentUserId, transaction) => { + const tx = ensureTransaction(transaction); + const customer = tx.customer; + if (currentUserId && currentUserId.uuid && tx.id && customer.id) { + // user can be either customer or provider + return currentUserId.uuid === customer.id.uuid + ? TX_TRANSITION_ACTOR_CUSTOMER + : TX_TRANSITION_ACTOR_PROVIDER; + } else { + throw new Error(`Parameters for "userIsCustomer" function were wrong. + currentUserId: ${currentUserId}, transaction: ${transaction}`); + } +}; + +export const txRoleIsProvider = userRole => userRole === TX_TRANSITION_ACTOR_PROVIDER; +export const txRoleIsCustomer = userRole => userRole === TX_TRANSITION_ACTOR_CUSTOMER; From bb5141eb50c76f41450576aaf2d8919679eded1d Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 30 Jan 2019 16:11:02 +0200 Subject: [PATCH 12/12] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d926f69d..9333e3d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2019-XX-XX +- [change] Extracted and refactored utility functions related to transaction and refactored several + components that show transaction data (incl. InboxPage, TransactionPanel, ActivityFeed). Before + updating your customization project, you should read more about what has changed from the pull + request. [#1004](https://github.com/sharetribe/flex-template-web/pull/1004) - [change] Rest of the documentation moved to Flex Docs: https://www.sharetribe.com/docs/ [#1015](https://github.com/sharetribe/flex-template-web/pull/1015)