我有一个可以添加产品的应用程序。 这些产品将显示在摘要页面中,如下所示:
出什么问题了? 默认情况下,应检查“摘要”页面上的产品。那是应该实现的,但并非在每种情况下都行得通。
基本上,我有一些反应路由器路线的导航。
这就是我所使用的:
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux-session": "^1.0.5",
"redux-thunk": "^2.3.0",
现在我正在使用history.push()和history.replace()重定向到摘要组件。
如果我是
所以我认为必须有一个竞争条件,但我找不到它。
有人可以在这里帮忙吗?切换项目的相关功能是toggleAll()
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link, Redirect, withRouter } from 'react-router-dom';
// eslint-disable-next-line
import { hot } from 'react-hot-loader';
import Checkbox from '@material-ui/core/Checkbox';
import SnackBar from '@material-ui/core/Snackbar';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import Tooltip from '@material-ui/core/Tooltip';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import SaveConfigDialog from './SaveConfigDialog';
import ShareConfigDialog from './ShareConfigDialog';
import SaveConfigResultDialog from './SaveConfigResultDialog';
import EtailerLightbox from '../shared/etailer-lightbox';
import DeleteConfirmation from '../delete-confirmation';
import { removeProduct } from '../../store/product-selection/actions';
import Notification from '../notification';
import ProductLine from './product-line';
// import { getCookies, setCookie } from '../../utils/cookie';
import { sessionId, fireEvent } from '../../utils/gtm-helper';
import { getProductTitle } from '../../utils/product-helper';
import { addProductIds } from '../../data/service/product-cart-cookie';
import { translate } from '../../utils/translations';
import { icon } from '../../utils/icons';
import Selection from '../../data/model/Selection';
import ProductCollection from '../../data/model/product-collection';
import Configuration from '../../data/model/configuration';
import ConfigurationDataService from '../../data/service/ConfigurationDataService';
import { withConfigurationData } from '../../context/ConfigurationDataContext';
const propTypes = {
// the selection of products to be rendered
globalProductSelection: PropTypes.instanceOf(Selection).isRequired,
// a function to be called when removing a product from the selection
onRemoveProduct: PropTypes.func,
// currently loaded Configuration can be null
loadedConfiguration: PropTypes.instanceOf(Configuration),
// the data service to use for saving the configuration
configurationDataService: PropTypes.instanceOf(ConfigurationDataService)
.isRequired,
};
const defaultProps = {
// by default, do nothing
onRemoveProduct: Function.prototype,
loadedConfiguration: null,
};
/**
* renders a summary of the current selection
* @warning some mild refactoring might be in order
* @todo find better names for the internal and global product selection
* @todo document what the internal selection is being used for
*/
class Summary extends React.Component {
constructor(props) {
super(props);
const { globalProductSelection } = this.props;
const products = globalProductSelection.getProducts();
this.state = {
// indicates which products are selected inside this summary
// do not confuse this with the selection that should actually be shown
selected: new ProductCollection(...products.items.concat()),
// true if the etailer lightbox should be shown
showEtailerLightbox: false,
showFeedback: false,
// true if the "save configuration" dialog should be shown
saveConfigDialog: false,
// true if the "configuration saved" dialog should be shown
saveConfigResultDialog: false,
isRequestByShareCode: false,
shareCodeNotAvailable: false,
showCurrentSelectionGetsOverwrittenDialog: false,
};
this.shareCode = null;
this.trackingSessions = {
addToCartSession: null,
compareSession: null,
shareSession: null,
};
}
componentDidMount() {
const { isRequestByShareCode } = this.state;
const { shareCode } = this.props;
if (shareCode && !isRequestByShareCode) {
this.setState({ isRequestByShareCode: true });
this.shareCode = shareCode;
this.checkForUnsavedCurrentSeletion();
}
}
checkForUnsavedCurrentSeletion() {
const { configurationDataService } = this.props;
const currentSelectedProductAmount = parseInt(
localStorage.getItem('product-advisor-productsamount') || '0',
10
);
return configurationDataService
.isThisShareCodeValid(this.shareCode)
.then((isConfigValid) => {
if (isConfigValid) {
if (currentSelectedProductAmount > 0) {
return this.showOverwriteDialog();
}
return this.handleCurrentSelectionIsEmpty();
}
return this.handleConfigIsNotValid();
});
}
handleCurrentSelectionIsEmpty() {
this.loadConfigIntoWizard(this.shareCode);
const { history } = this.props;
if (history.location.pathname.includes('share')) {
history.replace('/selection/summary');
}
}
handleConfigIsNotValid() {
this.setState({ shareCodeNotAvailable: true });
}
showOverwriteDialog() {
this.setState({ showCurrentSelectionGetsOverwrittenDialog: true });
}
loadConfigIntoWizard(shareCode) {
const { configurationDataService } = this.props;
return configurationDataService
.getConfigurationByShareCode(shareCode)
.then(configuration =>
configurationDataService.loadIntoWizard(configuration));
}
/**
* removes a product from the given selection and the internal selection as well
* @param {Product} product
*/
removeProduct(product) {
if (!product) return;
this.setState(
({ selected }) => {
selected.removeProduct(product);
return {
selected: new ProductCollection(...selected.items),
};
},
() => this.props.onRemoveProduct(product)
);
}
/**
* empties the current selection (and the internal selection as well)
*/
removeAllSelectedProducts() {
this.state.selected.forEach(product => this.removeProduct(product));
// deselect everything - everything was removed
this.setState({ selected: new ProductCollection(...[]) });
}
/**
* toggles the selection of all products
* - if not all products are selected, select all
* - if all products are selected, select none
*/
toggleAll(forceShow) {
const { globalProductSelection } = this.props;
if (
this.state.selected.length() <
globalProductSelection.getProducts().length() ||
forceShow
) {
this.setState({
selected: new ProductCollection(...globalProductSelection.getProducts().items),
});
} else {
this.setState({
selected: new ProductCollection(...[]),
});
}
}
/**
* toggles the selection of the given product
* @param {Product} product
*/
toggleSelection(product) {
const currentSelection = this.state.selected;
if (currentSelection.hasProduct(product)) {
currentSelection.removeProduct(product);
} else {
currentSelection.addProduct(product);
}
this.setState({
selected: new ProductCollection(...currentSelection.items),
});
}
storeProductsToCookie() {
/*
to create a unique id within a cart for each
configuration we use a sessionId for temp configs
*/
addProductIds(
this.state.selected.map(product => `${product.id}`),
`Cart_${sessionId}`
).then(() => {
/* Tracking */
if (this.trackingSessions.addToCartSession !== `${sessionId}`) {
this.trackingSessions.addToCartSession = `${sessionId}`;
this.state.selected.forEach((product) => {
fireEvent({
category: 'Product Advisor_beta_Configurations',
label: `Cart_${sessionId}`,
action: `${product.name} - ${product.category.name ||
product.productCategory}`,
});
});
}
});
}
handleCancelClick() {
this.shareCode = null;
this.setState({ showCurrentSelectionGetsOverwrittenDialog: false });
this.props.history.goBack();
}
handleOkClick() {
const { history } = this.props;
this.loadConfigIntoWizard(this.shareCode).then(() => {
const { globalProductSelection } = this.props;
this.setState({
selected: new ProductCollection(...globalProductSelection.getProducts().items),
showCurrentSelectionGetsOverwrittenDialog: false,
});
if (history.location.pathname.includes('share')) {
history.replace('/selection/summary');
}
});
}
buildDialog() {
return (
<Dialog open className="productAdvisor-shareConfig" maxWidth={false}>
<DialogTitle>
{translate('product.summary.shareConfigByUrl.selectionGetsOverwritten')}
</DialogTitle>
<DialogActions className="myDisplayActions">
<Button
color="primary"
className="performConfigInsertOrReplaceOK"
onClick={() => {
this.handleOkClick();
}}
>
{translate('product.summary.shareConfigByUrl.btn.overwrite')}
</Button>
<Button
color="primary"
className="button-gray btn performConfigInsertOrReplaceCancel"
onClick={() => {
this.handleCancelClick();
}}
>
{translate('product.summary.shareConfigByUrl.btn.cancel')}
</Button>
</DialogActions>
</Dialog>
);
}
render() {
const {
globalProductSelection,
loadedConfiguration,
isDisabled,
} = this.props;
const {
shareCodeNotAvailable,
isRequestByShareCode,
showCurrentSelectionGetsOverwrittenDialog,
} = this.state;
if (isDisabled) return <Redirect to="/selection/summary/" />;
// if (isDisabled) return <Redirect to="/selection/camera" />;
const products = globalProductSelection.getProductsOrderedByCategoryAndSubCategory();
if (!globalProductSelection.hasProducts() && !isRequestByShareCode) {
return <Notification message={translate('summary.noselection')} />;
}
if (shareCodeNotAvailable) {
return <Notification message="ShareCode not available" />;
}
if (showCurrentSelectionGetsOverwrittenDialog && !!this.shareCode) {
return this.buildDialog();
}
const firstSelectedProduct = this.state.selected.at(0);
const { showEtailerLightbox } = this.state;
const resultTableCSS = { position: 'relative' };
return (
<div className="type1 summary_content">
<EtailerLightbox
show={showEtailerLightbox}
product={firstSelectedProduct}
onClose={() => {
this.setState({
showEtailerLightbox: false,
});
}}
onAddToCart={() => {
this.storeProductsToCookie();
this.setState({
showEtailerLightbox: false,
});
// Website-Function to toggle cart
window.addToCartTimeout();
}}
/>
<Table
className="result-table product-advisor-table"
style={resultTableCSS}
>
<TableHead>
<TableRow>
<TableCell className="summary-checkbox-column">
<Checkbox
checked={this.state.selected.length() > 0}
disableRipple
color={
this.state.selected.length() === products.length()
? 'secondary'
: 'default'
}
onChange={() => this.toggleAll()}
/>
</TableCell>
<TableCell colSpan={4}>
<div className="table-header-row">
{this.state.selected.length > 0 ? (
<div className="selected-products">
<div className="selected-products-line-one">
{translate('summary.items.you_have')}
</div>
<div className="selected-products-line-two">
{this.state.selected.length()}
{this.state.selected.length() > 0
? translate('summary.items.selected')
: translate('summary.item.selected')}
</div>
</div>
) : (
''
)}
<div className="table-header-buttons">
<Tooltip
title={
this.state.selected.length === 0
? translate('summary.tooltips.add-to-cart')
: ''
}
>
<span>
<a
className="button btn-cta btn-white btn-flush-summary"
disabled={this.state.selected.length === 0}
onClick={() => {
this.setState({
openDeletionAlert: true,
});
}}
>
​
<div className="icn-external icn-alone">
<img
src={icon('general.details.trashorange')}
alt={translate('product.summary.flush')}
/>
</div>
</a>
</span>
</Tooltip>
<Link
className="button btn-white"
to="/saved/configs/overview"
>
{translate('overview.myconfigs')}
</Link>
<a
className="button btn-white shareConfigInSummary"
size="small"
onClick={() => {
this.setState({ showShareConfigDialog: true });
}}
>
{translate('product.summary.shareConfig')}
</a>
<a
className="button btn-white compareConfigsInSummary"
disabled={this.state.selected.length === 0}
size="small"
onClick={() => {
const doTrack = () => {
if (
this.trackingSessions.compareSession !==
`${sessionId}`
) {
this.trackingSessions.compareSession = `${sessionId}`;
this.state.selected.forEach((product) => {
fireEvent({
category: 'Product Advisor_beta_Configurations',
label: `WatchList_${sessionId}`,
// I'm not sure if we need the old getProductTitle call,
// so I'm leaving it in
// @TODO investigate if we actually need this
action: `${getProductTitle(product) ||
product.name} - ${product.productCategory ||
product.category.name}`,
});
});
}
};
this.setState({ saveConfigDialog: true });
doTrack();
}}
>
{translate('product.summary.compare')}
</a>
<Tooltip
title={
this.state.selected.length === 0
? translate('summary.tooltips.add-to-cart')
: ''
}
>
<span>
<a
className="button add-to-cart"
disabled={this.state.selected.length === 0}
onClick={() => {
if (
this.state.selected.length() === 1 &&
this.state.selected.at(0).etailer
) {
this.setState({
showEtailerLightbox: true,
});
} else {
this.storeProductsToCookie();
// Website-Function to toggle cart
window.addToCartTimeout();
}
}}
>
{translate('product.cart.addtocart')}
<div className="icn-external icn-add-to-cart">
<img
src={icon('general.details.addtocartwhite')}
alt={translate('product.cart.addtocart')}
/>
</div>
</a>
</span>
</Tooltip>
</div>
</div>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{products.map(product => (
<ProductLine
product={product}
key={product.id}
productIsSelected={this.state.selected.hasProduct(product)}
onSelect={p => this.toggleSelection(p)}
onRemoveProduct={removedProduct =>
this.removeProduct(removedProduct)
}
/>
))}
</TableBody>
</Table>
{this.state.saveConfigDialog && (
<SaveConfigDialog
configuration={loadedConfiguration}
onClose={() => this.setState({ saveConfigDialog: false })}
onSave={() => this.setState({ saveConfigResultDialog: true })}
/>
)}
{this.state.saveConfigResultDialog && (
<SaveConfigResultDialog
onClose={() =>
this.setState({
saveConfigResultDialog: false,
saveConfigDialog: false,
})
}
/>
)}
<SnackBar
autoHideDuration={5000}
message={translate('summary.success-feedback')}
open={this.state.showFeedback}
disableWindowBlurListener
anchorOrigin={{
horizontal: 'center',
vertical: 'center',
}}
onClose={() =>
this.setState({
showFeedback: false,
})
}
/>
<DeleteConfirmation
open={this.state.openDeletionAlert}
onClose={() =>
this.setState({
openDeletionAlert: false,
})
}
onConfirm={() =>
this.setState(
{
openDeletionAlert: false,
},
() => this.removeAllSelectedProducts()
)
}
/>
{this.state.showShareConfigDialog && (
<ShareConfigDialog
open={this.state.showShareConfigDialog}
configuration={loadedConfiguration}
saveOnComponentMount
onClose={() =>
this.setState({
showShareConfigDialog: false,
})
}
/>
)}
</div>
);
}
}
Summary.propTypes = propTypes;
Summary.defaultProps = defaultProps;
const stateMapper = ({
productSelection: { selected, loadedConfiguration },
}) => ({
globalProductSelection: selected,
loadedConfiguration,
});
export default hot(module)(connect(
stateMapper,
dispatch => ({
onRemoveProduct: product => dispatch(removeProduct(product)),
})
)(withRouter(withConfigurationData(Summary))));