diff --git a/libraries/commerce/cart/actions/addCurrentProductToCart.js b/libraries/commerce/cart/actions/addCurrentProductToCart.js deleted file mode 100644 index 1e6df5b34d..0000000000 --- a/libraries/commerce/cart/actions/addCurrentProductToCart.js +++ /dev/null @@ -1,42 +0,0 @@ -import Event from '@shopgate/pwa-core/classes/Event'; -import { EVENT_ADD_TO_CART_MISSING_VARIANT } from '../constants'; -import { getCurrentProduct } from '../../product/selectors/product'; -import { - getAddToCartOptions, - getAddToCartMetadata, -} from '../selectors'; -import addProductsToCart from './addProductsToCart'; - -/** - * Adds the current product to the cart. - * @return {Function} A redux thunk. - */ -const addCurrentProductToCart = () => (dispatch, getState) => { - const state = getState(); - const { quantity, productVariantId } = state.product.currentProduct; - const product = getCurrentProduct(state); - - let productId = null; - - if (!product.flags.hasVariants) { - productId = product.id; - } else if (productVariantId) { - productId = productVariantId; - } - - if (productId) { - const options = getAddToCartOptions(state); - const metadata = getAddToCartMetadata(state, productId, productVariantId); - - dispatch(addProductsToCart([{ - productId, - quantity, - ...(options) && { options }, - ...(metadata) && { metadata }, - }])); - } else { - Event.call(EVENT_ADD_TO_CART_MISSING_VARIANT); - } -}; - -export default addCurrentProductToCart; diff --git a/libraries/commerce/cart/actions/addProductsToCart.js b/libraries/commerce/cart/actions/addProductsToCart.js index 5ccd4b6f32..c2c988c050 100644 --- a/libraries/commerce/cart/actions/addProductsToCart.js +++ b/libraries/commerce/cart/actions/addProductsToCart.js @@ -6,22 +6,34 @@ import addProductsToCart from '../action-creators/addProductsToCart'; import successAddProductsToCart from '../action-creators/successAddProductsToCart'; import errorAddProductsToCart from '../action-creators/errorAddProductsToCart'; import setCartProductPendingCount from '../action-creators/setCartProductPendingCount'; -import { getProductPendingCount } from '../selectors'; +import { getProductPendingCount, getAddToCartOptions } from '../selectors'; +import { getProductMetadata } from '../../product/selectors/product'; import { messagesHaveErrors } from '../helpers'; /** * Adds a product to the cart. - * @param {Array} productData The options for the products to be added. + * @param {Object} data The pieces for the products to be added. * @return {Function} A redux thunk. */ -const addProductToCart = productData => (dispatch, getState) => { - const pendingProductCount = getProductPendingCount(getState()); +const addProductToCart = data => (dispatch, getState) => { + const state = getState(); + const pendingProductCount = getProductPendingCount(state); + const options = getAddToCartOptions(state, data); + const metadata = getProductMetadata(state, data.productId); + const products = [ + { + productId: data.productId, + quantity: data.quantity, + ...options && { options }, + ...metadata && { metadata }, + }, + ]; - dispatch(addProductsToCart(productData)); + dispatch(addProductsToCart(products)); dispatch(setCartProductPendingCount(pendingProductCount + 1)); const request = new PipelineRequest(pipelines.SHOPGATE_CART_ADD_PRODUCTS); - request.setInput({ products: productData }) + request.setInput({ products }) .setResponseProcessed(PROCESS_SEQUENTIAL) .setRetries(0) .dispatch() @@ -34,7 +46,7 @@ const addProductToCart = productData => (dispatch, getState) => { * but a messages array within the response payload. So by now we also have to dispatch * the error action here. */ - dispatch(errorAddProductsToCart(productData, messages, requestsPending)); + dispatch(errorAddProductsToCart(products, messages, requestsPending)); return; } @@ -42,7 +54,7 @@ const addProductToCart = productData => (dispatch, getState) => { }) .catch((error) => { const requestsPending = request.hasPendingRequests(); - dispatch(errorAddProductsToCart(productData, undefined, requestsPending)); + dispatch(errorAddProductsToCart(products, undefined, requestsPending)); logger.error(pipelines.SHOPGATE_CART_ADD_PRODUCTS, error); }); }; diff --git a/libraries/commerce/cart/selectors/index.js b/libraries/commerce/cart/selectors/index.js index 865e6aba94..d0c823353b 100644 --- a/libraries/commerce/cart/selectors/index.js +++ b/libraries/commerce/cart/selectors/index.js @@ -180,26 +180,6 @@ export const getFlags = createSelector( ({ flags }) => flags || {} ); -/** - * Builds the data for the 'metadata' property of addProductsToCart pipeline request payload. - * @returns {Object|null} The data if it was determinable, otherwise NULL. - */ -export const getAddToCartMetadata = createSelector( - getProductMetadata, - getSelectedVariantMetadata, - (metaData, variantMetaData) => { - if (variantMetaData) { - // Use the metadata from the getProductVariants data if available. - return variantMetaData; - } else if (metaData) { - // Use the metadata from the selected product if available. - return metaData; - } - - return null; - } -); - /** * Builds the data for the 'options' property of addProductsToCart pipeline request payload. * @param {Object} state The application state. @@ -209,7 +189,7 @@ export const getAddToCartOptions = createSelector( hasProductOptions, areProductOptionsSet, getRawProductOptions, - getCurrentProductOptions, + (state, props) => props.options, (hasOptions, areOptionsSet, options, currentOptions) => { // Check if options are ready to be added to a pipeline request. if (!hasOptions || !areOptionsSet) { diff --git a/libraries/commerce/product/action-creators/setProductOption.js b/libraries/commerce/product/action-creators/setProductOption.js deleted file mode 100644 index 9f3060d4d2..0000000000 --- a/libraries/commerce/product/action-creators/setProductOption.js +++ /dev/null @@ -1,15 +0,0 @@ -import { SET_PRODUCT_OPTION } from '../constants'; - -/** - * Dispatches the SET_PRODUCT_OPTION action. - * @param {string} optionId The ID of the option. - * @param {string} valueId The ID of the selected value. - * @return {Object} The SET_PRODUCT_OPTIONS action. - */ -const setProductOption = (optionId, valueId) => ({ - type: SET_PRODUCT_OPTION, - optionId, - valueId, -}); - -export default setProductOption; diff --git a/libraries/commerce/product/selectors/options.js b/libraries/commerce/product/selectors/options.js index 14af1b2b21..32a84fc314 100644 --- a/libraries/commerce/product/selectors/options.js +++ b/libraries/commerce/product/selectors/options.js @@ -1,20 +1,20 @@ import { createSelector } from 'reselect'; import { validateSelectorParams } from '@shopgate/pwa-common/helpers/data'; -import { getCurrentProductId, getCurrentProduct, getProductCurrency } from '../selectors/product'; +import { + getProductState, + getCurrentProduct, + getProductCurrency, +} from '../selectors/product'; /** * Retrieves the product options state. * @param {Object} state The application state. * @returns {Object} The product options state. */ -const getProductOptionsState = state => state.product.optionsByProductId; - -/** - * Retrieves the current options for the active product. - * @param {Object} state The application state. - * @returns {Object} - */ -export const getCurrentProductOptions = state => state.product.currentProduct.options; +const getProductOptionsState = createSelector( + getProductState, + state => state.optionsByProductId +); /** * Finds a product option item by the option id and item id. @@ -36,17 +36,48 @@ const findProductOptionItem = (options, optionId, itemId) => ( * @returns {Object} The product options. */ export const getRawProductOptions = createSelector( + (state, props) => props.productId, getProductOptionsState, - getCurrentProductId, - (productOptionsState, productId) => { + (productId, productOptionsState) => { const productOptions = productOptionsState[productId]; + if (!productOptions || productOptions.isFetching) { return null; } + return productOptions.options; } ); +// TODO: This needs to be optimized! +const getOptionItems = createSelector( + options => options, + (options, values) => values, + (options, values, option) => option, + (options, values, option, selected) => selected, + (options, values, option, selected, currency) => currency, + (options, values = [], option, selected, currency) => values.map((value) => { + // Add prices to each item that are relative to the current total product price. + if (!selected) { + return { + label: value.label, + currency, + value: value.id, + price: value.unitPriceModifier, + }; + } + + const selectedItem = findProductOptionItem(options, option.id, selected); + + return { + label: value.label, + currency, + value: value.id, + price: (value.unitPriceModifier - selectedItem.unitPriceModifier), + }; + }) +); + /** * Retrieves the current products options and transforms it to the correct data structure. * @param {Object} state The application state. @@ -54,37 +85,15 @@ export const getRawProductOptions = createSelector( */ export const getProductOptions = createSelector( getProductCurrency, - getCurrentProductOptions, + (state, props) => props.currentOptions, getRawProductOptions, validateSelectorParams((currency, currentOptions, options) => ( - options.map((option) => { - const selected = currentOptions[option.id]; - - return { - id: option.id, - label: option.label, - type: option.type, - items: (option.values || []).map(({ id, label, unitPriceModifier }) => { - // Add prices to each item that are relative to the current total product price. - if (!selected) { - return { - label, - currency, - value: id, - price: unitPriceModifier, - }; - } - - const selectedItem = findProductOptionItem(options, option.id, selected); - return { - label, - currency, - value: id, - price: unitPriceModifier - selectedItem.unitPriceModifier, - }; - }), - }; - }) + options.map(option => ({ + id: option.id, + label: option.label, + type: option.type, + items: getOptionItems(options, option.values, option, currentOptions[option.id], currency), + })) )) ); @@ -111,7 +120,7 @@ export const hasProductOptions = createSelector( */ export const areProductOptionsSet = createSelector( getRawProductOptions, - getCurrentProductOptions, + (state, props) => props.options, validateSelectorParams((options, currentOptions) => ( options.length === Object.keys(currentOptions).length )) diff --git a/libraries/commerce/product/selectors/price.js b/libraries/commerce/product/selectors/price.js index 74a04c9063..72297ca8d1 100644 --- a/libraries/commerce/product/selectors/price.js +++ b/libraries/commerce/product/selectors/price.js @@ -5,7 +5,6 @@ import { getProductUnitPrice, } from './product'; import { - getCurrentProductOptions, getRawProductOptions, hasProductOptions, areProductOptionsSet, @@ -21,13 +20,13 @@ import { * @returns {number} */ export const getProductPriceAddition = createSelector( - getCurrentProductOptions, + (state, props) => props.options, getRawProductOptions, validateSelectorParams( - (currentOptions, productOptions) => { + (options, productOptions) => { // Get all item modifiers. - const modifiers = Object.keys(currentOptions).map((optionId) => { - const itemId = currentOptions[optionId]; + const modifiers = Object.keys(options).map((optionId) => { + const itemId = options[optionId]; const optionItems = productOptions.find(item => item.id === optionId).values; const selectedItem = optionItems.find(item => item.id === itemId); diff --git a/libraries/commerce/product/selectors/product.js b/libraries/commerce/product/selectors/product.js index 99d4076c76..120ca8ad5f 100644 --- a/libraries/commerce/product/selectors/product.js +++ b/libraries/commerce/product/selectors/product.js @@ -11,12 +11,21 @@ import { ITEM_PATH } from '../constants'; import { getActiveFilters } from '../../filter/selectors'; import { filterProperties } from '../helpers'; +/** + * @param {Object} state The current application state. + * @return {Object} + */ +export const getProductState = state => state.product; + /** * Selects all products from the store. * @param {Object} state The current application state. * @return {Object} The collection of products. */ -export const getProducts = state => state.product.productsById; +export const getProducts = createSelector( + getProductState, + state => state.productsById +); /** * Retrieves the current product or variant page from the store. @@ -120,14 +129,16 @@ export const getProductUnitPrice = createSelector( * @param {Object} state The application state. * @returns {string} */ -export const getProductCurrency = (state) => { - const currentProduct = getCurrentProduct(state); - if (!currentProduct) { - return null; - } +export const getProductCurrency = createSelector( + getCurrentProduct, + (product) => { + if (!product) { + return null; + } - return currentProduct.price.currency; -}; + return product.price.currency; + } +); /** * Retrieves the generated result hash for a category ID. diff --git a/themes/gmd b/themes/gmd index 08d6f1aaec..69d7cc1d2c 160000 --- a/themes/gmd +++ b/themes/gmd @@ -1 +1 @@ -Subproject commit 08d6f1aaec398b63707fb90062a42987a0819da3 +Subproject commit 69d7cc1d2c76513925cb6fed29bdc86383cb97df