【问题标题】:How to add custom module into d365 commerce buybox module如何将自定义模块添加到 d365 commercebuybox 模块中
【发布时间】:2022-01-18 00:52:41
【问题描述】:

我正在尝试实现 excel 导入以将多个产品添加到购物车,为此我创建了自定义模块,其中添加了 excel 导入控件,但我试图将其调用到动态 365 商务产品详细信息页面的buybox 模块中,但它没有显示,请你帮我理解我做错了什么,我的代码如下。

product-quickorder.tsx 文件代码:

import { CSVReader } from 'react-papaparse';
import { IProductQuickorderData } from './product-quickorder.data';
import { IProductQuickorderProps } from './product-quickorder.props.autogenerated';
import { observable } from 'mobx';
import React from 'react';

// export interface IProductQuickorderViewProps extends 
IProductQuickorderProps<IProductQuickorderData> {}

class ProductQuickorder extends 
React.PureComponent<IProductQuickorderProps<IProductQuickorderData>>{
@observable
handleOnDrop = (data: object) => {debugger;
console.log('---------------------------');
console.log(data);
console.log('---------------------------');
  if(typeof data !='undefined')
  {
    for(var i=1;i<data.length;i++)
    {
      const query =data[i].data[0];       
   }
 }
}
handleOnFileLoad = (data: any) => {debugger;
    console.log('---------------------------');
    console.log(data);
    console.log('---------------------------');
  }

public render(): JSX.Element {debugger;
    return (
      <div>
      <CSVReader
      onDrop={this.handleOnDrop}
      // onFileLoad={this.handleOnFileLoad}
      onError={this.handleOnError}
      addRemoveButton
      // removeButtonColor='#659cef'
      onRemoveFile={this.handleOnRemoveFile}
    >
      <span>Drop CSV file here or click to upload.</span>
    </CSVReader>
     {/* <div className="board-row">{this.getProductsData()}</div> */}
     </div>
      );
}

handleOnError = (err: any, file: any, inputElem: any, reason: any) => {debugger;
    console.log(err);
  }

  handleOnRemoveFile = (data: any) => {debugger;
    console.log('---------------------------')
    console.log(data)
    console.log('---------------------------')
  }   
}
export default ProductQuickorder;

此文件的输出:

buybox.tsx 文件代码:

    /*--------------------------------------------------------------
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * See License.txt in the project root for license information.
 *--------------------------------------------------------------*/

/* eslint-disable no-duplicate-imports */
import ProductQuickorder from '../product-quickorder/product-quickorder';
import IProductQuickorderData from '../product-quickorder/product-quickorder';
import IProductQuickorderProps from '../product-quickorder/product-quickorder';
import { ICoreContext } from '@msdyn365-commerce/core-internal';
import { ProductDeliveryOptions, ProductPrice, ReleasedProductType, SimpleProduct } from '@msdyn365-commerce/retail-proxy';
import {
    ArrayExtensions, DeliveryMode, FinitePromiseQueue, FinitePromiseQueueError,
    getDeliveryOptionsForSelectedVariant, GetDeliveryOptionsForSelectedVariantInput,
    getDimensionsForSelectedVariant, GetDimensionsForSelectedVariantInput, getInventoryLevelCodeFromDimensionValue,
    getPriceForSelectedVariant, getProductAvailabilitiesForSelectedVariant, getSelectedVariant, IDimensionsApp, IDimensionValueForSelectedVariant,
    InventoryLevelValues, IProductInventoryInformation, IPromiseQueue, PriceForSelectedVariantInput,
    ProductAvailabilitiesForSelectedVariantInput, SelectedVariantInput, setDimensionValuesToQuery
} from '@msdyn365-commerce-modules/retail-actions';
import { getTelemetryObject, IModuleProps, INodeProps, ITelemetryContent } from '@msdyn365-commerce-modules/utilities';
import classnames from 'classnames';
import * as React from 'react';

import {
    getBuyboxAddToCart, getBuyBoxInventoryLabel, getBuyboxKeyInPrice,
    getBuyboxProductAddToOrderTemplate, getBuyboxProductAddToWishlist,
    getBuyboxProductDescription, getBuyboxProductPrice, getBuyboxProductQuantity,
    getBuyboxProductRating, getBuyboxProductTitle, getBuyboxProductUnitOfMeasure,
    getBuyboxShopSimilarButton, getQuantityLimitsMessages,
    RetailDefaultOrderQuantityLimitsFeatureName
} from '@msdyn365-commerce-modules/buybox';
import { getBuyboxProductConfigure } from '@msdyn365-commerce-modules/buybox';
import {
    IBuyboxAddToCartViewProps, IBuyboxAddToOrderTemplateViewProps, IBuyboxAddToWishlistViewProps,
    IBuyboxCallbacks, IBuyboxCommonData, IBuyboxExtentedProps, IBuyboxKeyInPriceViewProps,
    IBuyboxProductConfigureViewProps, IBuyboxProductQuantityViewProps, IBuyboxShopSimilarLookViewProps,
    IBuyboxState, IErrorState, ShopSimiliarButtonType } from '@msdyn365-commerce-modules/buybox';
import { IBuyboxData } from './buybox.data';
import { IBuyboxProps, IBuyboxResources } from './buybox.props.autogenerated';
import { getBuyboxFindInStore, IBuyboxFindInStoreViewProps } from './components/buybox-find-in-store';


export interface IBuyboxViewProps extends IBuyboxProps<IBuyboxData> {
    state: IBuyboxState;
    ModuleProps: IModuleProps;
    ProductInfoContainerProps: INodeProps;
    MediaGalleryContainerProps: INodeProps;
    callbacks: IBuyboxCallbacks;
    mediaGallery?: React.ReactNode;
    title?: React.ReactNode;
    description?: React.ReactNode;
    rating?: React.ReactNode;
    price?: React.ReactNode;
    addToOrderTemplate?: IBuyboxAddToOrderTemplateViewProps;
    addToWishlist?: IBuyboxAddToWishlistViewProps;
    max: number | undefined;
    addToCart: IBuyboxAddToCartViewProps;
    findInStore?: IBuyboxFindInStoreViewProps;
    quantity?: IBuyboxProductQuantityViewProps;
    configure?: IBuyboxProductConfigureViewProps;
    inventoryLabel?: React.ReactNode;
    shopSimilarLook?: IBuyboxShopSimilarLookViewProps;
    shopSimilarDescription?: IBuyboxShopSimilarLookViewProps;
    quantityLimitsMessages: React.ReactNode;
    telemetryContent?: ITelemetryContent;
    keyInPrice?: IBuyboxKeyInPriceViewProps;
    unitOfMeasure?: React.ReactNode;
    prodQuickOrd?: React.ReactNode;
}

/**
 * Buybox Module.
 */
class Buybox extends React.PureComponent<IBuyboxProps<IBuyboxData>, IBuyboxState> {
    /**
     * A queue of tasks of processing the changes in the dimensions.
     * Limit to two processes:
     * 1 - for the current process, which is under execution at the moment.
     * 2 - next process, which will process the latest version of data.
     * @remark Enqueueing new promises will discard the previous ones (except the one which is under processing).
     */
    private readonly dimensionUpdateQueue: IPromiseQueue<void> = new FinitePromiseQueue<void>(2);

    private dimensions: { [id: number]: string } = {};

    private readonly buyboxCallbacks: IBuyboxCallbacks = {
        updateQuantity: (newQuantity: number): boolean => {
            const errorState = { ...this.state.errorState };
            errorState.quantityError = undefined;
            errorState.otherError = undefined;

            this.setState({ quantity: newQuantity, errorState });
            return true;
        },
        updateErrorState: (newErrorState: IErrorState): void => {
            this.setState({ errorState: newErrorState });
        },
        updateSelectedProduct: (
            newSelectedProduct: Promise<SimpleProduct | null>,
            newInventory: IProductInventoryInformation | undefined,
            newPrice: ProductPrice | undefined,
            newDeliveryOptions: ProductDeliveryOptions | undefined
        ): void => {
            this.setState({
                selectedProduct: newSelectedProduct,
                productAvailableQuantity: newInventory,
                productDeliveryOptions: newDeliveryOptions
            });
            this._updatePrice(newPrice);
        },
        dimensionSelectedAsync: async (selectedDimensionId: number, selectedDimensionValueId: string): Promise<void> => {
            this.dimensions[selectedDimensionId] = selectedDimensionValueId;
            return this.dimensionUpdateQueue.enqueue(async () => {
                return this._updateDimensions();
            }).catch((error: any) => { // Ignore discarded processes.
                if (error !== FinitePromiseQueueError.ProcessWasDiscardedFromTheQueue) {
                    throw error;
                }
            });
        },
        getDropdownName: (dimensionType: number, resources: IBuyboxResources): string => {
            return this._getDropdownName(dimensionType, resources);
        },
        changeModalOpen: (isModalOpen: boolean): void => {
            this.setState({ modalOpen: isModalOpen });
        },
        changeUpdatingDimension: (isUpdatingDimension: boolean): void => {
            this.setState({ isUpdatingDimension });
        },

        /**
         * Update isUpdatingDeliveryOptions state.
         *
         * @param isUpdatingDeliveryOptions - The status of updating delivery options.
         */
        changeUpdatingDeliveryOptions: (isUpdatingDeliveryOptions: boolean): void => {
            this.setState({ isUpdatingDeliveryOptions });
        },

        updateKeyInPrice: (customPrice: number): void => {
            // Remove custom amount error when updating the custom price
            const errorState = { ...this.state.errorState };
            errorState.customAmountError = undefined;

            this.setState({ isPriceKeyedIn: true, keyInPriceAmount: customPrice, errorState });
            this._updatePrice(this.state.productPrice, customPrice);
        }
    };

    private readonly telemetryContent: ITelemetryContent;

    constructor(props: IBuyboxProps<IBuyboxData>, state: IBuyboxState) {
        super(props);
        this.state = {
            errorState: {
                configureErrors: {}
            },
            quantity: 1,
            min: undefined,
            max: undefined,
            selectedProduct: undefined,
            productPrice: undefined,
            productDeliveryOptions: undefined,

            modalOpen: false,
            isUpdatingDimension: false,
            isUpdatingDeliveryOptions: false
        };
        this.telemetryContent = getTelemetryObject(props.context.request.telemetryPageName!, props.friendlyName, props.telemetry);
    }

    public async componentDidMount(): Promise<void> {
        const {
            data: {
                product: { result: product },
                productPrice
            }
        } = this.props;

        productPrice.then(async (result) => {
            this._updatePrice(result);
        });

        if (product) {
            // Check if the product is service or not by product type
            if (product.ItemTypeValue === ReleasedProductType.Service) {
                this.setState({ isServiceItem: true });
            }
            await this._updateQuantitiesInState(product);
        }
    }

    public render(): JSX.Element | null {debugger;
    
        const {
            slots: {
                mediaGallery
            },
            data: {
                product: { result: product }
            },
            config: { className = '' }
        } = this.props;

        const
            {
                max
            } = this.state;

        if (!product) {
            this.props.context.telemetry.error('Product content is empty, module wont render');
            return null;
        }

        const props = this.props as IBuyboxExtentedProps<IBuyboxCommonData>;
        const defaultMinimumKeyInPrice = 10;
        const defaultMaximumKeyInPrice = 100;
        const props2 = this.props as IProductQuickorderProps<IProductQuickorderData>;
        const context = this.props.context as ICoreContext<IDimensionsApp>;
        const inventoryLevel = context.app.config.inventoryLevel;
        const dimensionValuesWithInventory = ArrayExtensions.flatten(
            ArrayExtensions.validValues(this.props.data.productDimensions.result?.map(value => value.dimensionValuesWithInventory)));
        const hasAvailableProducts = !ArrayExtensions.hasElements(dimensionValuesWithInventory) || dimensionValuesWithInventory.some(
            value => {
                const inventoryLevelCode = getInventoryLevelCodeFromDimensionValue(value, inventoryLevel);
                return inventoryLevelCode !== InventoryLevelValues.outOfStock;
            }
        );

        const viewProps: IBuyboxViewProps = {
            ...(this.props as IBuyboxProps<IBuyboxData>),
            state: this.state,
            mediaGallery: mediaGallery && mediaGallery.length > 0 ? mediaGallery[0] : undefined,
            ModuleProps: {
                moduleProps: this.props,
                className: classnames('ms-buybox', className)
            },
            ProductInfoContainerProps: {
                className: 'ms-buybox__content'
            },
            MediaGalleryContainerProps: {
                className: 'ms-buybox__media-gallery'
            },
            telemetryContent: this.telemetryContent,
            callbacks: this.buyboxCallbacks,
            title: getBuyboxProductTitle(props),
            description: getBuyboxProductDescription(props),
            unitOfMeasure: getBuyboxProductUnitOfMeasure(props),
            configure: getBuyboxProductConfigure(props, this.state, this.buyboxCallbacks),
            findInStore: getBuyboxFindInStore(this.props, this.state, this.buyboxCallbacks),
            price: getBuyboxProductPrice(props, this.state),
             prodQuickOrd:new ProductQuickorder(props2),
            addToCart: getBuyboxAddToCart(
                props, this.state, this.buyboxCallbacks, defaultMinimumKeyInPrice, defaultMaximumKeyInPrice, undefined, hasAvailableProducts),
            addToOrderTemplate: getBuyboxProductAddToOrderTemplate(props, this.state, this.buyboxCallbacks),
            addToWishlist: getBuyboxProductAddToWishlist(props, this.state, this.buyboxCallbacks),
            rating: !props.context.app.config.hideRating && getBuyboxProductRating(props),
            quantity: product.IsGiftCard ? undefined : getBuyboxProductQuantity(props, this.state, this.buyboxCallbacks),
            inventoryLabel: getBuyBoxInventoryLabel(props),
            shopSimilarLook: this.props.config.enableShopSimilarLooks && !product.IsGiftCard ? getBuyboxShopSimilarButton(props, ShopSimiliarButtonType.Looks) : undefined,
            shopSimilarDescription: this.props.config.enableShopSimilarDescription && !product.IsGiftCard ? getBuyboxShopSimilarButton(props, ShopSimiliarButtonType.Description) : undefined,
            keyInPrice: this.props.config.enableKeyInPrice && this.state.isCustomPriceSelected ? getBuyboxKeyInPrice(props, this.state, this.buyboxCallbacks) : undefined,
            quantityLimitsMessages: getQuantityLimitsMessages(props, this.state),
            max
        };

        return this.props.renderView(viewProps) as React.ReactElement;
    }

    private _updatePrice(
        newPrice: ProductPrice | undefined,
        customPrice: number | undefined = this.state.keyInPriceAmount): void {

        if (this.state.isCustomPriceSelected && newPrice) {
            newPrice.CustomerContextualPrice = customPrice;
        }
        this.setState({ productPrice: newPrice });
    }

    private readonly _updateDimensions = async (): Promise<void> => {
        const {
            data: {
                product: { result: product }
            },
            context: {
                actionContext,
                request: {
                    apiSettings: {
                        channelId
                    }
                }
            }
        } = this.props;

        const productDimensions = this.props.data.productDimensions.result;

        const hasProductDimensions = ArrayExtensions.hasElements(productDimensions);

        if (!product || !hasProductDimensions) {
            return;
        }

        const dimensionsToUpdate: { [id: number]: string } = { ...this.dimensions };
        this.setState({ isUpdatingDimension: true, isUpdatingDeliveryOptions: true });

        // Step 1: Clear error state to display relevant errors
        if (this.state.errorState.otherError || this.state.errorState.quantityError) {
            const clearErrorState = { ...this.state.errorState };
            clearErrorState.otherError = undefined;
            if (this.state.errorState.errorHost === 'ADDTOCART') {
                clearErrorState.quantityError = undefined;
                clearErrorState.errorHost = undefined;
            }
            this.setState({ errorState: clearErrorState });
        }

        // Step 2: Clear any errors indicating the dimension wasn't selected
        for (const key of Object.keys(dimensionsToUpdate)) {
            if (this.state.errorState.configureErrors[key]) {
                const errorState = { ...this.state.errorState };
                errorState.configureErrors[key] = undefined;

                this.setState({ errorState });
            }
        }

        const variantProduct: SimpleProduct | null = await this._updateProductDimensionsWithAvailabilities(dimensionsToUpdate);

        if (variantProduct) {
            // Step 5. Use these dimensions hydrate the inventory. Wrap this in a promise
            // so that places like add to cart can await it
            const newAvailableQuantity = await getProductAvailabilitiesForSelectedVariant(
                new ProductAvailabilitiesForSelectedVariantInput(
                    variantProduct.RecordId,
                    channelId
                ),
                actionContext
            );

            const newShippingQuantity = newAvailableQuantity?.find(shipping => shipping.deliveryType === DeliveryMode.shipping);
            const isCustompriceSelected = variantProduct.Dimensions?.find(
                dimension => dimension.DimensionTypeValue === 4 && dimension.DimensionValue && dimension.DimensionValue.Value?.toLowerCase() === 'custom');
            if (isCustompriceSelected) {
                this.setState({ isCustomPriceSelected: true });
            } else {
                // Remove custom amount error when unselect the custom amount
                const errorState = { ...this.state.errorState };
                errorState.customAmountError = undefined;

                this.setState({ isCustomPriceSelected: false, isPriceKeyedIn: false, errorState });
            }

            if (newShippingQuantity) {
                this.setState({ productAvailableQuantity: newShippingQuantity });
            } else {
                this.setState({ productAvailableQuantity: undefined });
            }

            // Step 6. Use these dimensions hydrate the product price.
            const newPrice = await getPriceForSelectedVariant(
                new PriceForSelectedVariantInput(
                    variantProduct.RecordId,
                    channelId
                ),
                actionContext
            );

            if (newPrice) {
                this._updatePrice(newPrice);
            }

            const retailMultiplePickupFeatureState = this.props.data.featureState.result?.find(
                featureState => featureState.Name === 'Dynamics.AX.Application.RetailMultiplePickupDeliveryModeFeature');

            // Step 7. Use these dimensions hydrate the product delivery options.
            const newDeliveryOptions = await getDeliveryOptionsForSelectedVariant(
                new GetDeliveryOptionsForSelectedVariantInput(
                    variantProduct.RecordId,
                    channelId,
                    undefined,
                    undefined,
                    retailMultiplePickupFeatureState?.IsEnabled
                ),
                actionContext
            );

            if (newDeliveryOptions) {
                this.setState({ productDeliveryOptions: newDeliveryOptions });
            }

            await this._updateQuantitiesInState(variantProduct);
        }
    };

    private async _updateProductDimensionsWithAvailabilities(dimensionsToUpdate: { [id: number]: string }): Promise<SimpleProduct | null> {
        const {
            data: {
                product: { result: product }
            },
            context: {
                actionContext,
                request: {
                    apiSettings: {
                        channelId
                    }
                }
            }
        } = this.props;

        const productDimensions = this.props.data.productDimensions.result!;

        // Step 3, Build the actually selected dimensions, prioritizing the information in state
        // over the information in data
        const mappedDimensions = productDimensions.map(dimension => {

            const dimensions = (dimension.dimensionValuesWithInventory ?? dimension.DimensionValues ?? []) as IDimensionValueForSelectedVariant[];
            return {
                DimensionTypeValue: dimension.DimensionTypeValue,
                DimensionValue: dimensions.find(
                    value => value.DimensionValue?.RecordId.toString() === dimensionsToUpdate[dimension.DimensionTypeValue]
                )?.DimensionValue ?? dimension.DimensionValue,
                ExtensionProperties: dimension.ExtensionProperties
            };
        }).filter(dimension => dimension.DimensionValue);
        setDimensionValuesToQuery(this.props.context.actionContext.requestContext.url.requestUrl, mappedDimensions);

        // Step 4. Use these dimensions hydrate the product. Wrap this in a promise
        // so that places like add to cart can await it
        const selectedProductPromise = getSelectedVariant(
            new SelectedVariantInput(
                product!.MasterProductId ? product!.MasterProductId : product!.RecordId,
                channelId
            ),
            actionContext
        );
        this.setState({ selectedProduct: selectedProductPromise });

        const variantProduct = await selectedProductPromise;
        if (variantProduct) {
            await getDimensionsForSelectedVariant(
                new GetDimensionsForSelectedVariantInput(
                    variantProduct.MasterProductId ? variantProduct.MasterProductId : variantProduct.RecordId,
                    channelId,
                    mappedDimensions
                ),
                actionContext
            );
        }

        return variantProduct;
    }

    private async _updateQuantitiesInState(product: SimpleProduct): Promise<void> {
        const isOrderQuantityLimitsFeatureEnabled = await this._isOrderQuantityLimitsFeatureEnabled();
        if (isOrderQuantityLimitsFeatureEnabled && product) {
            this.setState({
                quantity: product.Behavior?.DefaultQuantity || 1,
                min: product.Behavior?.MinimumQuantity || 1,

                // If max by feature in default order settings is not defined then use max from site settings or default max 10.
                max: (product.Behavior?.MaximumQuantity && product.Behavior?.MaximumQuantity > 0) ? product.Behavior?.MaximumQuantity : this.props.context.app.config.maxQuantityForCartLineItem || 10
            });
        } else {
            this.setState({
                min: 1,
                max: this.props.context.app.config.maxQuantityForCartLineItem || 10
            });
        }
    }

    private readonly _getDropdownName = (dimensionType: number, resources: IBuyboxResources): string => {
        const isGiftCard = this.props.data.product.result?.IsGiftCard;

        switch (dimensionType) {
            case 1: // ProductDimensionType.Color
                return resources.productDimensionTypeColor;
            case 2: // ProductDimensionType.Configuration
                return resources.productDimensionTypeConfiguration;
            case 3: // ProductDimensionType.Size
                return resources.productDimensionTypeSize;
            case 4: // ProductDimensionType.Style
                return isGiftCard ? resources.productDimensionTypeAmount : resources.productDimensionTypeStyle;
            default:
                return '';
        }
    };

    private async _isOrderQuantityLimitsFeatureEnabled(): Promise<boolean> {
        const defaultOrderQuantityLimitsFeatureConfig = this.props.context?.request?.app?.platform?.enableDefaultOrderQuantityLimits;
        if (defaultOrderQuantityLimitsFeatureConfig === 'none') {
            return false;
        }

        const featureStatuses = await this.props.data.featureState;
        const isFeatureEnabledInHq = featureStatuses?.find(featureState => featureState.Name === RetailDefaultOrderQuantityLimitsFeatureName)?.IsEnabled;
        if (!isFeatureEnabledInHq) {
            return false;
        }

        if (defaultOrderQuantityLimitsFeatureConfig === 'all') {
            return true;
        }
        let customerInfo;
        try {
            customerInfo = await this.props.data.customerInformation;
        } catch (error) {
            //this.props.telemetry.information(error);
            this.props.telemetry.debug('Unable to receive Customer Information. May be user is not authorized');
            return false;
        }

        return customerInfo &&
            ((defaultOrderQuantityLimitsFeatureConfig === 'b2b' && customerInfo.IsB2b) ||
                (defaultOrderQuantityLimitsFeatureConfig === 'b2c' && !customerInfo.IsB2b));
    }
}

export default Buybox;

在运行商业网站并导航到产品详细信息页面后,它不显示文件上传选项。

感谢和问候,
桑迪

【问题讨论】:

    标签: typescript microsoft-dynamics commerce


    【解决方案1】:

    我可以添加自定义模块,现在可以在buybox 模块中显示自定义模块。首先我已经初始化了自定义模块对象并调用了 render 方法来加载 react.element,请参考下面的代码。

      /*--------------------------------------------------------------
     * Copyright (c) Microsoft Corporation. All rights reserved.
     * See License.txt in the project root for license information.
     *--------------------------------------------------------------*/
    
    /* eslint-disable no-duplicate-imports */
    import { ICoreContext } from '@msdyn365-commerce/core-internal';
    import { ProductDeliveryOptions, ProductPrice, ReleasedProductType, SimpleProduct } from '@msdyn365-commerce/retail-proxy';
    import {
        ArrayExtensions, DeliveryMode, FinitePromiseQueue, FinitePromiseQueueError,
        getDeliveryOptionsForSelectedVariant, GetDeliveryOptionsForSelectedVariantInput,
        getDimensionsForSelectedVariant, GetDimensionsForSelectedVariantInput, getInventoryLevelCodeFromDimensionValue,
        getPriceForSelectedVariant, getProductAvailabilitiesForSelectedVariant, getSelectedVariant, IDimensionsApp, IDimensionValueForSelectedVariant,
        InventoryLevelValues, IProductInventoryInformation, IPromiseQueue, PriceForSelectedVariantInput,
        ProductAvailabilitiesForSelectedVariantInput, SelectedVariantInput, setDimensionValuesToQuery
    } from '@msdyn365-commerce-modules/retail-actions';
    import { getTelemetryObject, IModuleProps, INodeProps, ITelemetryContent } from '@msdyn365-commerce-modules/utilities';
    import classnames from 'classnames';
    import * as React from 'react';
    
    import {
        getBuyboxAddToCart, getBuyBoxInventoryLabel, getBuyboxKeyInPrice,
        getBuyboxProductAddToOrderTemplate, getBuyboxProductAddToWishlist,
        getBuyboxProductDescription, getBuyboxProductPrice, getBuyboxProductQuantity,
        getBuyboxProductRating, getBuyboxProductTitle, getBuyboxProductUnitOfMeasure,
        getBuyboxShopSimilarButton, getQuantityLimitsMessages,
        RetailDefaultOrderQuantityLimitsFeatureName
    } from '@msdyn365-commerce-modules/buybox';
    import { getBuyboxProductConfigure } from '@msdyn365-commerce-modules/buybox';
    import {
        IBuyboxAddToCartViewProps, IBuyboxAddToOrderTemplateViewProps, IBuyboxAddToWishlistViewProps,
        IBuyboxCallbacks, IBuyboxCommonData, IBuyboxExtentedProps, IBuyboxKeyInPriceViewProps,
        IBuyboxProductConfigureViewProps, IBuyboxProductQuantityViewProps, IBuyboxShopSimilarLookViewProps,
        IBuyboxState, IErrorState, ShopSimiliarButtonType } from '@msdyn365-commerce-modules/buybox';
    import { IBuyboxData } from './buybox.data';
    import { IBuyboxProps, IBuyboxResources } from './buybox.props.autogenerated';
    import { getBuyboxFindInStore, IBuyboxFindInStoreViewProps } from './components/buybox-find-in-store';
    // import ModalBody from 'reactstrap/lib/ModalBody';
    import ProductQuickorder from '../product-quickorder/product-quickorder';
    import IProductQuickorderData from '../product-quickorder/product-quickorder';
    import IProductQuickorderProps from '../product-quickorder/product-quickorder';
    
    export interface IBuyboxViewProps extends IBuyboxProps<IBuyboxData> {
        state: IBuyboxState;
        ModuleProps: IModuleProps;
        ProductInfoContainerProps: INodeProps;
        MediaGalleryContainerProps: INodeProps;
        callbacks: IBuyboxCallbacks;
        mediaGallery?: React.ReactNode;
        title?: React.ReactNode;
        description?: React.ReactNode;
        rating?: React.ReactNode;
        price?: React.ReactNode;
        addToOrderTemplate?: IBuyboxAddToOrderTemplateViewProps;
        addToWishlist?: IBuyboxAddToWishlistViewProps;
        max: number | undefined;
        addToCart: IBuyboxAddToCartViewProps;
        findInStore?: IBuyboxFindInStoreViewProps;
        quantity?: IBuyboxProductQuantityViewProps;
        configure?: IBuyboxProductConfigureViewProps;
        inventoryLabel?: React.ReactNode;
        shopSimilarLook?: IBuyboxShopSimilarLookViewProps;
        shopSimilarDescription?: IBuyboxShopSimilarLookViewProps;
        quantityLimitsMessages: React.ReactNode;
        telemetryContent?: ITelemetryContent;
        keyInPrice?: IBuyboxKeyInPriceViewProps;
        unitOfMeasure?: React.ReactNode;
        prodQuickOrd?: React.ReactElement;
    }
    
    /**
     * Buybox Module.
     */
    class Buybox extends React.PureComponent<IBuyboxProps<IBuyboxData>, IBuyboxState> {
        /**
         * A queue of tasks of processing the changes in the dimensions.
         * Limit to two processes:
         * 1 - for the current process, which is under execution at the moment.
         * 2 - next process, which will process the latest version of data.
         * @remark Enqueueing new promises will discard the previous ones (except the one which is under processing).
         */
        private readonly dimensionUpdateQueue: IPromiseQueue<void> = new FinitePromiseQueue<void>(2);
    
        private dimensions: { [id: number]: string } = {};
    
        private readonly buyboxCallbacks: IBuyboxCallbacks = {
            updateQuantity: (newQuantity: number): boolean => {
                const errorState = { ...this.state.errorState };
                errorState.quantityError = undefined;
                errorState.otherError = undefined;
    
                this.setState({ quantity: newQuantity, errorState });
                return true;
            },
            updateErrorState: (newErrorState: IErrorState): void => {
                this.setState({ errorState: newErrorState });
            },
            updateSelectedProduct: (
                newSelectedProduct: Promise<SimpleProduct | null>,
                newInventory: IProductInventoryInformation | undefined,
                newPrice: ProductPrice | undefined,
                newDeliveryOptions: ProductDeliveryOptions | undefined
            ): void => {
                this.setState({
                    selectedProduct: newSelectedProduct,
                    productAvailableQuantity: newInventory,
                    productDeliveryOptions: newDeliveryOptions
                });
                this._updatePrice(newPrice);
            },
            dimensionSelectedAsync: async (selectedDimensionId: number, selectedDimensionValueId: string): Promise<void> => {
                this.dimensions[selectedDimensionId] = selectedDimensionValueId;
                return this.dimensionUpdateQueue.enqueue(async () => {
                    return this._updateDimensions();
                }).catch((error: any) => { // Ignore discarded processes.
                    if (error !== FinitePromiseQueueError.ProcessWasDiscardedFromTheQueue) {
                        throw error;
                    }
                });
            },
            getDropdownName: (dimensionType: number, resources: IBuyboxResources): string => {
                return this._getDropdownName(dimensionType, resources);
            },
            changeModalOpen: (isModalOpen: boolean): void => {
                this.setState({ modalOpen: isModalOpen });
            },
            changeUpdatingDimension: (isUpdatingDimension: boolean): void => {
                this.setState({ isUpdatingDimension });
            },
    
            /**
             * Update isUpdatingDeliveryOptions state.
             *
             * @param isUpdatingDeliveryOptions - The status of updating delivery options.
             */
            changeUpdatingDeliveryOptions: (isUpdatingDeliveryOptions: boolean): void => {
                this.setState({ isUpdatingDeliveryOptions });
            },
    
            updateKeyInPrice: (customPrice: number): void => {
                // Remove custom amount error when updating the custom price
                const errorState = { ...this.state.errorState };
                errorState.customAmountError = undefined;
    
                this.setState({ isPriceKeyedIn: true, keyInPriceAmount: customPrice, errorState });
                this._updatePrice(this.state.productPrice, customPrice);
            }
        };
    
        private readonly telemetryContent: ITelemetryContent;
    
        constructor(props: IBuyboxProps<IBuyboxData>, state: IBuyboxState) {
            super(props);
            this.state = {
                errorState: {
                    configureErrors: {}
                },
                quantity: 1,
                min: undefined,
                max: undefined,
                selectedProduct: undefined,
                productPrice: undefined,
                productDeliveryOptions: undefined,
    
                modalOpen: false,
                isUpdatingDimension: false,
                isUpdatingDeliveryOptions: false
            };
            this.telemetryContent = getTelemetryObject(props.context.request.telemetryPageName!, props.friendlyName, props.telemetry);
        }
    
        public async componentDidMount(): Promise<void> {
            const {
                data: {
                    product: { result: product },
                    productPrice
                }
            } = this.props;
    
            productPrice.then(async (result) => {
                this._updatePrice(result);
            });
    
            if (product) {
                // Check if the product is service or not by product type
                if (product.ItemTypeValue === ReleasedProductType.Service) {
                    this.setState({ isServiceItem: true });
                }
                await this._updateQuantitiesInState(product);
            }
        }
    
        public render(): JSX.Element | null {
            
            const {
                slots: {
                    mediaGallery
                },
                data: {
                    product: { result: product }
                },
                config: { className = '' }
            } = this.props;
    
            const
                {
                    max
                } = this.state;
    
            if (!product) {
                this.props.context.telemetry.error('Product content is empty, module wont render');
                return null;
            }
    
            const props = this.props as IBuyboxExtentedProps<IBuyboxCommonData>;
            const defaultMinimumKeyInPrice = 10;
            const defaultMaximumKeyInPrice = 100;
            const props2 = this.props as IProductQuickorderProps<IProductQuickorderData>;
            const context = this.props.context as ICoreContext<IDimensionsApp>;
            const inventoryLevel = context.app.config.inventoryLevel;
            const dimensionValuesWithInventory = ArrayExtensions.flatten(
                ArrayExtensions.validValues(this.props.data.productDimensions.result?.map(value => value.dimensionValuesWithInventory)));
            const hasAvailableProducts = !ArrayExtensions.hasElements(dimensionValuesWithInventory) || dimensionValuesWithInventory.some(
                value => {
                    const inventoryLevelCode = getInventoryLevelCodeFromDimensionValue(value, inventoryLevel);
                    return inventoryLevelCode !== InventoryLevelValues.outOfStock;
                }
            );
             const prodQuickOrd2 = new ProductQuickorder(props2);
            const viewProps: IBuyboxViewProps = {
                ...(this.props as IBuyboxProps<IBuyboxData>),
                state: this.state,
                mediaGallery: mediaGallery && mediaGallery.length > 0 ? mediaGallery[0] : undefined,
                ModuleProps: {
                    moduleProps: this.props,
                    className: classnames('ms-buybox', className)
                },
                ProductInfoContainerProps: {
                    className: 'ms-buybox__content'
                },
                MediaGalleryContainerProps: {
                    className: 'ms-buybox__media-gallery'
                },
                telemetryContent: this.telemetryContent,
                callbacks: this.buyboxCallbacks,
                title: getBuyboxProductTitle(props),
                description: getBuyboxProductDescription(props),
                unitOfMeasure: getBuyboxProductUnitOfMeasure(props),
                configure: getBuyboxProductConfigure(props, this.state, this.buyboxCallbacks),
                findInStore: getBuyboxFindInStore(this.props, this.state, this.buyboxCallbacks),
                price: getBuyboxProductPrice(props, this.state),
                prodQuickOrd: prodQuickOrd2.render(),
                addToCart: getBuyboxAddToCart(
                    props, this.state, this.buyboxCallbacks, defaultMinimumKeyInPrice, defaultMaximumKeyInPrice, undefined, hasAvailableProducts),
                addToOrderTemplate: getBuyboxProductAddToOrderTemplate(props, this.state, this.buyboxCallbacks),
                addToWishlist: getBuyboxProductAddToWishlist(props, this.state, this.buyboxCallbacks),
                rating: !props.context.app.config.hideRating && getBuyboxProductRating(props),
                quantity: product.IsGiftCard ? undefined : getBuyboxProductQuantity(props, this.state, this.buyboxCallbacks),
                inventoryLabel: getBuyBoxInventoryLabel(props),
                shopSimilarLook: this.props.config.enableShopSimilarLooks && !product.IsGiftCard ? getBuyboxShopSimilarButton(props, ShopSimiliarButtonType.Looks) : undefined,
                shopSimilarDescription: this.props.config.enableShopSimilarDescription && !product.IsGiftCard ? getBuyboxShopSimilarButton(props, ShopSimiliarButtonType.Description) : undefined,
                keyInPrice: this.props.config.enableKeyInPrice && this.state.isCustomPriceSelected ? getBuyboxKeyInPrice(props, this.state, this.buyboxCallbacks) : undefined,
                quantityLimitsMessages: getQuantityLimitsMessages(props, this.state),
                max
            };
    
            return this.props.renderView(viewProps) as React.ReactElement;
        }
    
        private _updatePrice(
            newPrice: ProductPrice | undefined,
            customPrice: number | undefined = this.state.keyInPriceAmount): void {
    
            if (this.state.isCustomPriceSelected && newPrice) {
                newPrice.CustomerContextualPrice = customPrice;
            }
            this.setState({ productPrice: newPrice });
        }
    
        private readonly _updateDimensions = async (): Promise<void> => {
            const {
                data: {
                    product: { result: product }
                },
                context: {
                    actionContext,
                    request: {
                        apiSettings: {
                            channelId
                        }
                    }
                }
            } = this.props;
    
            const productDimensions = this.props.data.productDimensions.result;
    
            const hasProductDimensions = ArrayExtensions.hasElements(productDimensions);
    
            if (!product || !hasProductDimensions) {
                return;
            }
    
            const dimensionsToUpdate: { [id: number]: string } = { ...this.dimensions };
            this.setState({ isUpdatingDimension: true, isUpdatingDeliveryOptions: true });
    
            // Step 1: Clear error state to display relevant errors
            if (this.state.errorState.otherError || this.state.errorState.quantityError) {
                const clearErrorState = { ...this.state.errorState };
                clearErrorState.otherError = undefined;
                if (this.state.errorState.errorHost === 'ADDTOCART') {
                    clearErrorState.quantityError = undefined;
                    clearErrorState.errorHost = undefined;
                }
                this.setState({ errorState: clearErrorState });
            }
    
            // Step 2: Clear any errors indicating the dimension wasn't selected
            for (const key of Object.keys(dimensionsToUpdate)) {
                if (this.state.errorState.configureErrors[key]) {
                    const errorState = { ...this.state.errorState };
                    errorState.configureErrors[key] = undefined;
    
                    this.setState({ errorState });
                }
            }
    
            const variantProduct: SimpleProduct | null = await this._updateProductDimensionsWithAvailabilities(dimensionsToUpdate);
    
            if (variantProduct) {
                // Step 5. Use these dimensions hydrate the inventory. Wrap this in a promise
                // so that places like add to cart can await it
                const newAvailableQuantity = await getProductAvailabilitiesForSelectedVariant(
                    new ProductAvailabilitiesForSelectedVariantInput(
                        variantProduct.RecordId,
                        channelId
                    ),
                    actionContext
                );
    
                const newShippingQuantity = newAvailableQuantity?.find(shipping => shipping.deliveryType === DeliveryMode.shipping);
                const isCustompriceSelected = variantProduct.Dimensions?.find(
                    dimension => dimension.DimensionTypeValue === 4 && dimension.DimensionValue && dimension.DimensionValue.Value?.toLowerCase() === 'custom');
                if (isCustompriceSelected) {
                    this.setState({ isCustomPriceSelected: true });
                } else {
                    // Remove custom amount error when unselect the custom amount
                    const errorState = { ...this.state.errorState };
                    errorState.customAmountError = undefined;
    
                    this.setState({ isCustomPriceSelected: false, isPriceKeyedIn: false, errorState });
                }
    
                if (newShippingQuantity) {
                    this.setState({ productAvailableQuantity: newShippingQuantity });
                } else {
                    this.setState({ productAvailableQuantity: undefined });
                }
    
                // Step 6. Use these dimensions hydrate the product price.
                const newPrice = await getPriceForSelectedVariant(
                    new PriceForSelectedVariantInput(
                        variantProduct.RecordId,
                        channelId
                    ),
                    actionContext
                );
    
                if (newPrice) {
                    this._updatePrice(newPrice);
                }
    
                const retailMultiplePickupFeatureState = this.props.data.featureState.result?.find(
                    featureState => featureState.Name === 'Dynamics.AX.Application.RetailMultiplePickupDeliveryModeFeature');
    
                // Step 7. Use these dimensions hydrate the product delivery options.
                const newDeliveryOptions = await getDeliveryOptionsForSelectedVariant(
                    new GetDeliveryOptionsForSelectedVariantInput(
                        variantProduct.RecordId,
                        channelId,
                        undefined,
                        undefined,
                        retailMultiplePickupFeatureState?.IsEnabled
                    ),
                    actionContext
                );
    
                if (newDeliveryOptions) {
                    this.setState({ productDeliveryOptions: newDeliveryOptions });
                }
    
                await this._updateQuantitiesInState(variantProduct);
            }
        };
    
        private async _updateProductDimensionsWithAvailabilities(dimensionsToUpdate: { [id: number]: string }): Promise<SimpleProduct | null> {
            const {
                data: {
                    product: { result: product }
                },
                context: {
                    actionContext,
                    request: {
                        apiSettings: {
                            channelId
                        }
                    }
                }
            } = this.props;
    
            const productDimensions = this.props.data.productDimensions.result!;
    
            // Step 3, Build the actually selected dimensions, prioritizing the information in state
            // over the information in data
            const mappedDimensions = productDimensions.map(dimension => {
    
                const dimensions = (dimension.dimensionValuesWithInventory ?? dimension.DimensionValues ?? []) as IDimensionValueForSelectedVariant[];
                return {
                    DimensionTypeValue: dimension.DimensionTypeValue,
                    DimensionValue: dimensions.find(
                        value => value.DimensionValue?.RecordId.toString() === dimensionsToUpdate[dimension.DimensionTypeValue]
                    )?.DimensionValue ?? dimension.DimensionValue,
                    ExtensionProperties: dimension.ExtensionProperties
                };
            }).filter(dimension => dimension.DimensionValue);
            setDimensionValuesToQuery(this.props.context.actionContext.requestContext.url.requestUrl, mappedDimensions);
    
            // Step 4. Use these dimensions hydrate the product. Wrap this in a promise
            // so that places like add to cart can await it
            const selectedProductPromise = getSelectedVariant(
                new SelectedVariantInput(
                    product!.MasterProductId ? product!.MasterProductId : product!.RecordId,
                    channelId
                ),
                actionContext
            );
            this.setState({ selectedProduct: selectedProductPromise });
    
            const variantProduct = await selectedProductPromise;
            if (variantProduct) {
                await getDimensionsForSelectedVariant(
                    new GetDimensionsForSelectedVariantInput(
                        variantProduct.MasterProductId ? variantProduct.MasterProductId : variantProduct.RecordId,
                        channelId,
                        mappedDimensions
                    ),
                    actionContext
                );
            }
    
            return variantProduct;
        }
    
        private async _updateQuantitiesInState(product: SimpleProduct): Promise<void> {
            const isOrderQuantityLimitsFeatureEnabled = await this._isOrderQuantityLimitsFeatureEnabled();
            if (isOrderQuantityLimitsFeatureEnabled && product) {
                this.setState({
                    quantity: product.Behavior?.DefaultQuantity || 1,
                    min: product.Behavior?.MinimumQuantity || 1,
    
                    // If max by feature in default order settings is not defined then use max from site settings or default max 10.
                    max: (product.Behavior?.MaximumQuantity && product.Behavior?.MaximumQuantity > 0) ? product.Behavior?.MaximumQuantity : this.props.context.app.config.maxQuantityForCartLineItem || 10
                });
            } else {
                this.setState({
                    min: 1,
                    max: this.props.context.app.config.maxQuantityForCartLineItem || 10
                });
            }
        }
    
        private readonly _getDropdownName = (dimensionType: number, resources: IBuyboxResources): string => {
            const isGiftCard = this.props.data.product.result?.IsGiftCard;
    
            switch (dimensionType) {
                case 1: // ProductDimensionType.Color
                    return resources.productDimensionTypeColor;
                case 2: // ProductDimensionType.Configuration
                    return resources.productDimensionTypeConfiguration;
                case 3: // ProductDimensionType.Size
                    return resources.productDimensionTypeSize;
                case 4: // ProductDimensionType.Style
                    return isGiftCard ? resources.productDimensionTypeAmount : resources.productDimensionTypeStyle;
                default:
                    return '';
            }
        };
    
        private async _isOrderQuantityLimitsFeatureEnabled(): Promise<boolean> {
            const defaultOrderQuantityLimitsFeatureConfig = this.props.context?.request?.app?.platform?.enableDefaultOrderQuantityLimits;
            if (defaultOrderQuantityLimitsFeatureConfig === 'none') {
                return false;
            }
    
            const featureStatuses = await this.props.data.featureState;
            const isFeatureEnabledInHq = featureStatuses?.find(featureState => featureState.Name === RetailDefaultOrderQuantityLimitsFeatureName)?.IsEnabled;
            if (!isFeatureEnabledInHq) {
                return false;
            }
    
            if (defaultOrderQuantityLimitsFeatureConfig === 'all') {
                return true;
            }
            let customerInfo;
            try {
                customerInfo = await this.props.data.customerInformation;
            } catch (error) {
                //this.props.telemetry.information(error);
                this.props.telemetry.debug('Unable to receive Customer Information. May be user is not authorized');
                return false;
            }
    
            return customerInfo &&
                ((defaultOrderQuantityLimitsFeatureConfig === 'b2b' && customerInfo.IsB2b) ||
                    (defaultOrderQuantityLimitsFeatureConfig === 'b2c' && !customerInfo.IsB2b));
        }
    }
    
    export default Buybox;
    

    此文件的输出:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-08-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多