When bulk ordering complex industrial components or configurable B2B products, spreadsheet order upload features are vital for rapid purchasing. However, when an uploaded row encounters an error, such as an unconfigured safety valve or a missing bundle selection, users typically face a frustrating dead end.
Traditionally, users receive a static error message, forcing them to cancel the upload, navigate away, locate the product manually, configure it, and start the entire upload process over again.
To eliminate this friction, I designed and implemented a custom interactive error-resolution workflow in Optimizely Configured Commerce (using the Spire CMS / React architecture).
The Workflow Experience
Here is how the seamless error resolution workflow functions for the end user:
- Interactive Errors: Users click on a configurable product error directly inside the OrderUploadErrorsModal .
- Context-Aware Navigation: A new tab opens to the Product Details Page (PDP) with essential context parameters ( rowIndex , productId , fromOrderUpload ).
- Seamless Resolution: Once the user configures the item and clicks Add To Cart , cross-window messaging ( window.postMessage ) notifies the original tab.
- Instant Feedback: The PDP tab automatically closes, returning focus to the original modal where the resolved row immediately turns green, displaying: "Item has been added to the cart" .
Architectural Breakdown: How It Was Implemented
1. Extending the Batch Load Products handler & Mapping Product Metadata
To enable navigation to the PDP from an error row, the client application needs to know which product failed and where that product lives on the site.
In the extended BatchLoadProducts handler chain ( BatchLoadProducts.ts ):
- Defined an extended error interface: CustomOrderUploadRowError , which adds productId , productPath ( productDetailUrl ), and canConfigure .
export interface CustomOrderUploadRowError extends OrderUploadRowError {
productId?: string;
productPath?: string;
canConfigure?: boolean;
}- Customized ProcessProducts and mapRowError to capture these metadata fields:
function CustomProcessProducts(props: batchLoadProductsProps) {
const state = props.getState();
const currentPage = getCurrentPage(state);
const processState =
["VmiBinsPage", "VmiLocationDetailsPage"].indexOf(currentPage.type) !== -1
? state.components.vmiBinsImportModal.modalIsOpen
? state.components.vmiBinsImportModal
: state.components.vmiCountsImportModal
: state.components.orderUpload;
const products = props.result.apiResult;
if ("uploadCancelled" in processState && processState.uploadCancelled) {
return false;
}
for (let i = 0; i < products.length; i++) {
const item =
"uploadedItems" in processState
? processState.uploadedItems[i]
: "parsedItems" in processState
? processState.parsedItems[i]
: null;
if (item === null) {
continue;
}
const index = props.parameter.firstRowHeading ? i + 2 : i + 1;
const product = products[i];
if (product) {
const error = validateProduct(product, props.parameter.checkInventory);
if (error === UploadError.None) {
product.qtyOrdered = !item.qtyOrdered ? 1 : item.qtyOrdered;
const isInvalidUnitOfMeasure = setProductUnitOfMeasure(product, item, index, props.result.rowErrors);
if (!isInvalidUnitOfMeasure) {
addProductToList(product, item, index, props.result, props.parameter.checkInventory);
}
} else {
product.qtyOrdered = item.qtyOrdered ?? 0;
props.result.rowErrors.push(mapRowError(index, error, item.name, product));
}
} else {
props.result.rowErrors.push(
mapRowError(index, UploadError.NotFound, item.name, {
qtyOrdered: item.qtyOrdered,
unitOfMeasureDisplay: item.unitOfMeasure,
} as ProductDto),
);
}
}
}
const hasTrueProperty = (
product: ProductDto & { properties?: Record<string, string> },
key: string,
) => product.properties?.[key]?.toLowerCase() === "true";
const validateProduct = (
product: ProductDto & { properties?: Record<string, string> },
checkInventory: boolean) => {
if ((product.canConfigure || (product.isConfigured && !product.isFixedConfiguration))
|| (hasTrueProperty(product, "isBundle") || hasTrueProperty(product, "isSafetyValve"))) {
return UploadError.ConfigurableProduct;
}
if (product.isStyleProductParent) {
return UploadError.StyledProduct;
}
if (checkInventory && product.qtyOnHand === 0 && product.trackInventory && !product.canBackOrder) {
return UploadError.OutOfStock;
}
if (!product.canAddToCart) {
return UploadError.Unavailable;
}
// guard optional chaining in case getUnitNetPrice returns undefined/null
if (product && product.pricing && getUnitNetPrice(product.pricing, 1)?.price === 0) {
return UploadError.WrongType;
}
return UploadError.None;
};
const mapRowError = (
index: number,
error: UploadError,
name: string,
product: ProductDto & { properties?: Record<string, string> }
): CustomOrderUploadRowError => {
return {
index,
error,
name,
qtyRequested: product.qtyOrdered,
umRequested: product.unitOfMeasureDisplay,
qtyOnHand: product.qtyOnHand,
productId: product.id,
productPath: product.productDetailUrl, // or urlSegment depending on your API
canConfigure: (!hasTrueProperty(product, "isBundle") || !hasTrueProperty(product, "isSafetyValve")),
};
};
2. Interactive Links & Cross-Window Messaging in OrderUploadErrorsModal
Inside OrderUploadErrorsModal.tsx , update the rowErrors property in the mapStateToProps to inherit from the CustomOrderUploadRowError .
const mapStateToProps = (state: ApplicationState) => ({
errorsModalIsOpen: state.components.orderUpload.errorsModalIsOpen,
products: state.components.orderUpload.products,
rowErrors: state.components.orderUpload.rowErrors as CustomOrderUploadRowError[],
rowNoErrors: state.components.orderUpload.products,
isBadFile: state.components.orderUpload.isBadFile,
failedToLoadInventory: state.components.orderUpload.failedToLoadInventory,
uploadLimitExceeded: state.components.orderUpload.uploadLimitExceeded,
});Static error descriptions for configurable products are converted into interactive links ( ErrorLink ).
<DataTableCell {...styles.reasonCell} data-test-selector={`row_${index}_Reason`}>
{resolvedRows.includes(index) ? (
<span style={{ color: "#2e7d32", fontWeight: 600 }}>
{translate("Item has been added to the cart")}
</span>
) : (
<>
{canConfigure && productPath && !resolvedRows.includes(index) && (
<ErrorLink
href={productPath}
target="_blank"
rel="noopener noreferrer"
onClick={e => {
e.preventDefault();
handlePdpOpen({ index, productId, productPath });
}}
>
{error === UploadError.ConfigurableProduct && errorReasons[UploadError.ConfigurableProduct]}
</ErrorLink>
)}
<span style={{ color: "#c00", fontWeight: 500 }}>
{error === UploadError.NotEnough && errorReasons[UploadError.NotEnough]}
{error === UploadError.StyledProduct && errorReasons[UploadError.StyledProduct]}
{error === UploadError.Unavailable && errorReasons[UploadError.Unavailable]}
{error === UploadError.InvalidUnit && errorReasons[UploadError.InvalidUnit]}
{error === UploadError.NotFound && errorReasons[UploadError.NotFound]}
{error === UploadError.OutOfStock && errorReasons[UploadError.OutOfStock]}
{error === UploadError.WrongType && errorReasons[UploadError.WrongType]}
</span>
</>
)}
</DataTableCell>
Step A: Opening the PDP with Query Context
When a user clicks an error link, handlePdpOpen launches the PDP in a new window or tab while passing the row index:
const handlePdpOpen = (params: {
productId?: string;
index: number;
productPath?: string;
}) => {
if (!params.productPath) return;
const url = `params.productPath?fromOrderUpload=true&productId={params.productId}&rowIndex=${params.index}`;
window.open(url, "_blank");
};
Step B: Listening for Resolution via window.postMessage
The modal listens for incoming messages from the child tab. Upon receiving ORDER_UPLOAD_PRODUCT_ADDED , it appends the rowIndex to the resolvedRows local state array:
const [resolvedRows, setResolvedRows] = useState<number[]>([]);
React.useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === "ORDER_UPLOAD_PRODUCT_ADDED") {
const { rowIndex } = event.data;
// Track row index for UI state update
setResolvedRows(prev => [...prev, rowIndex]);
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, []);
Step C: Dynamic Row Styling & Status Highlights
When rendering each data row, the table checks if resolvedRows.includes(index) is true. If resolved, it updates the text color to green ( #2e7d32 ) and replaces the error message with a success confirmation:
<DataTableRow
key={index}
style={resolvedRows.includes(index) ? { color: "#2e7d32", fontWeight: 500 } : undefined}
>
{/* ... other table cells ... */}
<DataTableCell {...styles.reasonCell}>
{resolvedRows.includes(index) ? (
<span style={{ color: "#2e7d32", fontWeight: 600 }}>
{translate("Item has been added to the basket")}
</span>
) : (
/* Render Error Link / Message */
<ErrorLink onClick={() => handlePdpOpen(rowError)} />
)}
</DataTableCell>
</DataTableRow>
3. Closing the Feedback Loop in ProductAddToCartButton
On the PDP side, when the user finishes configuring their options (such as safety valves or bundle selections) and hits Add To Cart , ProductAddToCartButton.tsx executes the standard cart addition logic and then notifies the parent window.
const notifyParentAndClose = (productId: string, rowIndex: number) => {
if (window.opener) {
window.opener.postMessage(
{
type: "ORDER_UPLOAD_PRODUCT_ADDED",
productId,
rowIndex,
},
"*"
);
window.close();
}
};
const addToCartClickHandler = async () => {
// ... add to cart payload generation and execution ...
const cartline = await addToCart({/* ... */});
if (showAddToCartConfirmationDialog && showAddedToCartMessage) {
// Parse the rowIndex parameter from URL
const params = new URLSearchParams(window.location.search);
const rowIndex = Number(params.get("rowIndex"));
// Notify parent order upload modal and close child tab
notifyParentAndClose(productId, rowIndex);
}
};
Technical Summary & Key UX Benefits
| Feature / Step | Technical Mechanism | UX Benefit |
|---|---|---|
| Error Linking | Handlers inject productPath & productId into CustomOrderUploadRowError . | Direct navigation to the exact product requiring attention without losing upload context. |
| Cross-Window Communication | Browser window.postMessage() API between PDP child window and Order Upload parent window. | Instant synchronization between tabs without page reloads or state corruption. |
| Visual Feedback | React local state ( resolvedRows ) updates row typography and color dynamically. | Clear visual cues showing which errors are fixed and ready for cart checkout. |
By extending Optimizely's React handler chain and utilizing native browser window messaging, we transformed a frustrating error state into an intuitive, highly efficient resolution path. B2B buyers working with complex, configurable products can now resolve upload exceptions seamlessly without losing their place or progress.

