@evenicanpm/storefront-core 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +29 -9
- package/src/api-manager/datasources/d365/d365-address.datasource.ts +11 -0
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +23 -1
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +6 -1
- package/src/api-manager/index.ts +2 -1
- package/src/api-manager/schemas/product.schema.ts +1 -1
- package/src/auth/msal.ts +1 -4
- package/src/cms/blocks/block-manager.tsx +1 -1
- package/src/cms/endpoints.ts +7 -2
- package/src/components/BazaarMenu.tsx +1 -1
- package/src/components/Typography.tsx +4 -2
- package/src/components/carousel-cards/carousel-card-1/CarouselCard1.stories.tsx +1 -1
- package/src/components/categories/category-menu.tsx +1 -1
- package/src/components/countries-input.tsx +4 -0
- package/src/components/header/sticky-header.tsx +0 -1
- package/src/components/navbar/mobile-menu/mobile-menu.test.tsx +1 -1
- package/src/components/navbar/utils/transform-nav.test.ts +1 -1
- package/src/components/navbar/utils/transform-nav.ts +1 -1
- package/src/components/product-cards/product-card/product-card.tsx +5 -2
- package/src/components/product-cards/product-card/styles/index.ts +1 -5
- package/src/components/products-view/compound/products-grid-view.tsx +5 -1
- package/src/components/products-view/compound/products-list-view.tsx +5 -1
- package/src/global.d.ts +3 -0
- package/src/pages/account/addresses/address-form.tsx +20 -2
- package/src/pages/account/wishlist/wishlist-item.tsx +1 -2
- package/src/pages/cart/cart-item.tsx +1 -7
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +14 -3
- package/src/pages/checkout/checkout-alt-form/steps/address/address-card.tsx +5 -3
- package/src/pages/checkout/checkout-alt-form/steps/address/new-address-form.tsx +1 -1
- package/src/pages/confirmation/ordered-products.tsx +3 -1
- package/src/pages/product-details/bopis/find-in-store-modal.tsx +4 -4
- package/src/pages/product-details/bopis/pickup-option-select.tsx +2 -2
- package/src/pages/product-details/bopis/search-header.tsx +2 -2
- package/src/pages/product-details/product-intro/compound/context.ts +8 -3
- package/src/pages/product-details/product-intro/compound/product-info.tsx +25 -19
- package/src/pages/product-list/product-list-view.tsx +2 -1
- package/src/providers/nav-provider/index.tsx +1 -1
- package/src/providers/nav-provider/utils/createLink.ts +1 -1
- package/tsconfig.json +1 -2
- package/__mocks__/countries.ts +0 -11
- package/__mocks__/create-mutation.ts +0 -68
- package/__mocks__/create-query.ts +0 -94
- package/__mocks__/data/categories.json +0 -795
- package/__mocks__/get-product-by-id.ts +0 -6
- package/__mocks__/next-auth-react.ts +0 -9
- package/__mocks__/next-font.js +0 -4
- package/__mocks__/next-headers.js +0 -13
- package/__mocks__/next-image.tsx +0 -18
- package/__mocks__/next-link.js +0 -19
- package/__mocks__/next-navigation.ts +0 -29
- package/__mocks__/product-dimensions.ts +0 -635
- package/__mocks__/products.ts +0 -533
- package/__mocks__/query-client.ts +0 -3
- package/__mocks__/wishlists.json +0 -408
- package/src/cms/blog.ts +0 -229
- package/src/cms/interfaces/navigation.ts +0 -52
- package/src/cms/interfaces/product-data.ts +0 -83
- package/src/cms/pages.ts +0 -149
|
@@ -11,6 +11,7 @@ import type { CountryRegionInfo } from "@msdyn365-commerce/retail-proxy";
|
|
|
11
11
|
import {
|
|
12
12
|
Box,
|
|
13
13
|
Button,
|
|
14
|
+
CircularProgress,
|
|
14
15
|
FormControl,
|
|
15
16
|
InputLabel,
|
|
16
17
|
MenuItem,
|
|
@@ -25,7 +26,7 @@ import { withZodSchema } from "formik-validator-zod";
|
|
|
25
26
|
import _ from "lodash";
|
|
26
27
|
import { useRouter } from "next/navigation";
|
|
27
28
|
import { useLocale, useTranslations } from "next-intl";
|
|
28
|
-
import React, { createContext, useContext } from "react";
|
|
29
|
+
import React, { createContext, useContext, useRef } from "react";
|
|
29
30
|
import { z } from "zod";
|
|
30
31
|
|
|
31
32
|
interface AddressFormValues {
|
|
@@ -71,6 +72,7 @@ const AddressForm = ({ address, children }: RootProps) => {
|
|
|
71
72
|
|
|
72
73
|
const { mutateAsync: createAddress } = useCreateAddress();
|
|
73
74
|
const { mutateAsync: updateAddress } = useUpdateAddress();
|
|
75
|
+
const isSubmittingRef = useRef(false);
|
|
74
76
|
|
|
75
77
|
const FormSchema = z.object({
|
|
76
78
|
Name: z.string().nonempty(t("Validation.required", { field: t("name") })),
|
|
@@ -99,6 +101,9 @@ const AddressForm = ({ address, children }: RootProps) => {
|
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
const handleSubmit = async (values: AddressFormValues) => {
|
|
104
|
+
if (isSubmittingRef.current) return;
|
|
105
|
+
isSubmittingRef.current = true;
|
|
106
|
+
|
|
102
107
|
let newAddress: Address = {
|
|
103
108
|
Name: values.Name,
|
|
104
109
|
Street: values.Street,
|
|
@@ -128,6 +133,7 @@ const AddressForm = ({ address, children }: RootProps) => {
|
|
|
128
133
|
severity: "error",
|
|
129
134
|
});
|
|
130
135
|
console.log(error);
|
|
136
|
+
isSubmittingRef.current = false;
|
|
131
137
|
}
|
|
132
138
|
};
|
|
133
139
|
|
|
@@ -291,8 +297,20 @@ AddressForm.Actions = ({ children }: { children?: React.ReactNode }) => {
|
|
|
291
297
|
|
|
292
298
|
AddressForm.SubmitButton = () => {
|
|
293
299
|
const t = useTranslations("Address");
|
|
300
|
+
const ctx = useContext(FormContext);
|
|
301
|
+
const isSubmitting = ctx?.formik.isSubmitting ?? false;
|
|
294
302
|
return (
|
|
295
|
-
<Button
|
|
303
|
+
<Button
|
|
304
|
+
type="submit"
|
|
305
|
+
variant="contained"
|
|
306
|
+
color="primary"
|
|
307
|
+
disabled={isSubmitting}
|
|
308
|
+
startIcon={
|
|
309
|
+
isSubmitting ? (
|
|
310
|
+
<CircularProgress size={20} color="inherit" />
|
|
311
|
+
) : undefined
|
|
312
|
+
}
|
|
313
|
+
>
|
|
296
314
|
{t("save")}
|
|
297
315
|
</Button>
|
|
298
316
|
);
|
|
@@ -74,11 +74,10 @@ const WishlistItem = ({ wishlist, children, sx }: RootProps) => {
|
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
WishlistItem.Name = () => {
|
|
77
|
+
const { wishlists: wishlistsRoute } = useCreateAccountRoutes();
|
|
77
78
|
const ctx = useContext(WishlistItemContext);
|
|
78
79
|
if (!ctx) return null;
|
|
79
80
|
const { wishlist } = ctx;
|
|
80
|
-
const { wishlists: wishlistsRoute } = useCreateAccountRoutes();
|
|
81
|
-
|
|
82
81
|
return (
|
|
83
82
|
<Paragraph ellipsis>
|
|
84
83
|
<NavLink3
|
|
@@ -416,13 +416,6 @@ CartItemRoot.Comment = () => {
|
|
|
416
416
|
</FlexBox>
|
|
417
417
|
</>
|
|
418
418
|
);
|
|
419
|
-
} else if (isLoading) {
|
|
420
|
-
content = (
|
|
421
|
-
<CircularProgress
|
|
422
|
-
size={30}
|
|
423
|
-
sx={{ marginTop: "auto", marginLeft: "auto" }}
|
|
424
|
-
/>
|
|
425
|
-
);
|
|
426
419
|
} else {
|
|
427
420
|
content = (
|
|
428
421
|
<>
|
|
@@ -448,6 +441,7 @@ CartItemRoot.Comment = () => {
|
|
|
448
441
|
<Button
|
|
449
442
|
color="primary"
|
|
450
443
|
variant="outlined"
|
|
444
|
+
disabled={isLoading}
|
|
451
445
|
sx={{ width: 150, mt: "auto", ml: "auto" }}
|
|
452
446
|
onClick={() => setIsWritingComment(true)}
|
|
453
447
|
>
|
|
@@ -7,7 +7,7 @@ import { PageSlug } from "@evenicanpm/storefront-core/src/lib/page-slugs";
|
|
|
7
7
|
import CheckoutStep from "@evenicanpm/storefront-core/src/pages/checkout/checkout-alt-form/checkout-step";
|
|
8
8
|
import { useNotification } from "@evenicanpm/storefront-core/src/providers/notifications/use-notification";
|
|
9
9
|
import type { CardPaymentAcceptResult } from "@msdyn365-commerce/retail-proxy";
|
|
10
|
-
import { Box, Button } from "@mui/material";
|
|
10
|
+
import { Box, Button, CircularProgress } from "@mui/material";
|
|
11
11
|
import Grid from "@mui/material/Grid2";
|
|
12
12
|
import { useQueryClient } from "@tanstack/react-query";
|
|
13
13
|
import { useRouter } from "next/navigation";
|
|
@@ -18,6 +18,7 @@ import React, {
|
|
|
18
18
|
type ReactNode,
|
|
19
19
|
useContext,
|
|
20
20
|
useEffect,
|
|
21
|
+
useRef,
|
|
21
22
|
useState,
|
|
22
23
|
} from "react";
|
|
23
24
|
|
|
@@ -62,12 +63,15 @@ const CheckoutForm = ({ cart, children }: CheckoutFormProps) => {
|
|
|
62
63
|
const { data: channelConfigurationData } = getChannelConfiguration.useData();
|
|
63
64
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
64
65
|
const [totalSteps, setTotalSteps] = useState(0);
|
|
66
|
+
const isSubmittingRef = useRef(false);
|
|
65
67
|
|
|
66
68
|
const cleanupCart = async (transactionId: string) => {
|
|
67
69
|
router.push(`${PageSlug.Confirmation}?transactionId=${transactionId}`);
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
const placeOrder = async () => {
|
|
73
|
+
if (isSubmittingRef.current) return;
|
|
74
|
+
isSubmittingRef.current = true;
|
|
71
75
|
setIsSubmitting(true);
|
|
72
76
|
const cache = queryClient.getQueryCache();
|
|
73
77
|
|
|
@@ -98,6 +102,8 @@ const CheckoutForm = ({ cart, children }: CheckoutFormProps) => {
|
|
|
98
102
|
.data as CardPaymentAcceptResult;
|
|
99
103
|
|
|
100
104
|
if (!paymentAcceptResult?.TokenizedPaymentCard) {
|
|
105
|
+
isSubmittingRef.current = false;
|
|
106
|
+
setIsSubmitting(false);
|
|
101
107
|
return;
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -121,8 +127,9 @@ const CheckoutForm = ({ cart, children }: CheckoutFormProps) => {
|
|
|
121
127
|
message: t("placeOrderErrorMessage"),
|
|
122
128
|
severity: "error",
|
|
123
129
|
});
|
|
130
|
+
isSubmittingRef.current = false;
|
|
131
|
+
setIsSubmitting(false);
|
|
124
132
|
}
|
|
125
|
-
setIsSubmitting(false);
|
|
126
133
|
};
|
|
127
134
|
|
|
128
135
|
return (
|
|
@@ -227,7 +234,11 @@ CheckoutForm.Footer = () => {
|
|
|
227
234
|
color="primary"
|
|
228
235
|
onClick={placeOrder}
|
|
229
236
|
>
|
|
230
|
-
{
|
|
237
|
+
{isSubmitting ? (
|
|
238
|
+
<CircularProgress size={24} color="inherit" />
|
|
239
|
+
) : (
|
|
240
|
+
t("placeOrderButton")
|
|
241
|
+
)}
|
|
231
242
|
</Button>
|
|
232
243
|
</Grid>
|
|
233
244
|
);
|
|
@@ -115,13 +115,15 @@ AddressCard.Body = () => {
|
|
|
115
115
|
if (!ctx) return null;
|
|
116
116
|
|
|
117
117
|
const { address } = ctx;
|
|
118
|
+
// Join only non-empty values so a dangling comma is never rendered.
|
|
119
|
+
const stateRegion = [address.State, address.ThreeLetterISORegionName]
|
|
120
|
+
.filter((value) => value?.trim())
|
|
121
|
+
.join(", ");
|
|
118
122
|
return (
|
|
119
123
|
<>
|
|
120
124
|
<Paragraph color="grey.700">{address.Street}</Paragraph>
|
|
121
125
|
<Paragraph color="grey.700">{address.City}</Paragraph>
|
|
122
|
-
<Paragraph color="grey.700">
|
|
123
|
-
{address.State}, {address.ThreeLetterISORegionName}
|
|
124
|
-
</Paragraph>
|
|
126
|
+
{stateRegion && <Paragraph color="grey.700">{stateRegion}</Paragraph>}
|
|
125
127
|
<Paragraph color="grey.700">{address.ZipCode}</Paragraph>
|
|
126
128
|
<Paragraph color="grey.700">{address.Phone}</Paragraph>
|
|
127
129
|
</>
|
|
@@ -14,9 +14,9 @@ import DialogContent from "@mui/material/DialogContent";
|
|
|
14
14
|
import Grid from "@mui/material/Grid2";
|
|
15
15
|
import TextField from "@mui/material/TextField";
|
|
16
16
|
import { merge } from "lodash";
|
|
17
|
+
import { useTranslations } from "next-intl";
|
|
17
18
|
import React from "react";
|
|
18
19
|
import { Controller, type UseFormReturn, useForm } from "react-hook-form";
|
|
19
|
-
import { useTranslations } from "use-intl";
|
|
20
20
|
import z from "zod";
|
|
21
21
|
|
|
22
22
|
type AddressFormInput = {
|
|
@@ -117,7 +117,9 @@ OrderedProducts.Comment = ({ line }: LineProps) => {
|
|
|
117
117
|
if (!line.Comment) return null;
|
|
118
118
|
return (
|
|
119
119
|
<Paragraph mt={1}>
|
|
120
|
-
<
|
|
120
|
+
<Span sx={{ display: "inline", fontSize: 14, fontWeight: 600 }}>
|
|
121
|
+
{t("commentLabel")}:
|
|
122
|
+
</Span>
|
|
121
123
|
{line.Comment}
|
|
122
124
|
</Paragraph>
|
|
123
125
|
);
|
|
@@ -56,7 +56,7 @@ const initialSearchArea: SearchArea = {
|
|
|
56
56
|
* @param {Function} props.handleAddToCart - Function to handle adding the product to cart
|
|
57
57
|
* @param {number} props.productId - ID of the product being searched
|
|
58
58
|
*
|
|
59
|
-
* @returns {
|
|
59
|
+
* @returns {React.ReactElement} - Rendered modal component that displays available stores with inventory information
|
|
60
60
|
*
|
|
61
61
|
* @example
|
|
62
62
|
* <FindInStoreModal
|
|
@@ -71,7 +71,7 @@ const FindInStoreModal = ({
|
|
|
71
71
|
open,
|
|
72
72
|
handleAddToCart,
|
|
73
73
|
productId,
|
|
74
|
-
}: Props):
|
|
74
|
+
}: Props): React.ReactElement => {
|
|
75
75
|
const t = useTranslations("productDetail");
|
|
76
76
|
const [hideOutOfStock, setHideOutOfStock] = React.useState(false);
|
|
77
77
|
const { data: channelConfig } = getChannelConfiguration.useSuspenseData();
|
|
@@ -154,7 +154,7 @@ const FindInStoreModal = ({
|
|
|
154
154
|
const _stores = Object.values(stores);
|
|
155
155
|
if (hideOutOfStock) {
|
|
156
156
|
return _stores.filter(
|
|
157
|
-
(store) => !isOutOfStock(store
|
|
157
|
+
(store) => !isOutOfStock(store?.location?.OrgUnitNumber || ""),
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
160
|
return _stores;
|
|
@@ -205,7 +205,7 @@ const FindInStoreModal = ({
|
|
|
205
205
|
address={location.Address || ""}
|
|
206
206
|
actionArea={renderActionArea(
|
|
207
207
|
location.OrgUnitNumber || "",
|
|
208
|
-
pickupOptions || [],
|
|
208
|
+
(pickupOptions || []) as DeliveryOption[],
|
|
209
209
|
inventoryRecord?.TotalAvailableInventoryLevelLabel || "",
|
|
210
210
|
)}
|
|
211
211
|
openFrom={location.OpenFrom || 0}
|
|
@@ -31,7 +31,7 @@ type Props = {
|
|
|
31
31
|
* @param {boolean} props.isOutOfStock - Flag indicating if the product is out of stock at this location
|
|
32
32
|
* @param {string} props.inventoryLabel - Label to display when product is out of stock
|
|
33
33
|
*
|
|
34
|
-
* @returns {
|
|
34
|
+
* @returns {React.ReactElement} The rendered pickup option selection component
|
|
35
35
|
*
|
|
36
36
|
* @example
|
|
37
37
|
* <PickupOptionSelect
|
|
@@ -48,7 +48,7 @@ const PickupOptionSelect = ({
|
|
|
48
48
|
onPickupClick,
|
|
49
49
|
isOutOfStock,
|
|
50
50
|
inventoryLabel,
|
|
51
|
-
}: Props):
|
|
51
|
+
}: Props): React.ReactElement => {
|
|
52
52
|
const [selectedOption, setSelectedOption] =
|
|
53
53
|
React.useState<DeliveryOption | null>(pickupOptions[0] || null);
|
|
54
54
|
const handlePickupOptionChange = (event: SelectChangeEvent) => {
|
|
@@ -19,14 +19,14 @@ type Props = {
|
|
|
19
19
|
* @param {Object} props - Component props
|
|
20
20
|
* @param {Function} props.handleSearch - Callback function that handles the search action with the search term
|
|
21
21
|
*
|
|
22
|
-
* @returns {
|
|
22
|
+
* @returns {React.ReactElement} A form with a search input field that triggers the search on submit or Enter key
|
|
23
23
|
*
|
|
24
24
|
* @example
|
|
25
25
|
* ```tsx
|
|
26
26
|
* <SearchHeader handleSearch={(term) => console.log(`Searching for ${term}`)} />
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
const SearchHeader = ({ handleSearch }: Props):
|
|
29
|
+
const SearchHeader = ({ handleSearch }: Props): React.ReactElement => {
|
|
30
30
|
const [searchTerm, setSearchTerm] = React.useState<string>("");
|
|
31
31
|
const t = useTranslations("productDetail.Bopis");
|
|
32
32
|
const renderSearchAdornment = () => (
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ProductDetails,
|
|
3
|
+
ProductPrice,
|
|
4
|
+
} from "@evenicanpm/storefront-core/src/api-manager/schemas/product.schema";
|
|
2
5
|
import type { SelectedDimensions } from "@evenicanpm/storefront-core/src/hooks/use-variants";
|
|
3
6
|
import type {
|
|
4
7
|
ProductDimensionValueInventoryAvailability,
|
|
@@ -44,8 +47,10 @@ export interface ProductImagesContext {
|
|
|
44
47
|
export interface ProductInfoContext {
|
|
45
48
|
/** Does the product have variants? If not we do not verify dimensions before adding to cart etc. **/
|
|
46
49
|
hasVariants: boolean;
|
|
47
|
-
/**
|
|
48
|
-
|
|
50
|
+
/** Combined loading state for price and inventory */
|
|
51
|
+
isProductDetailsPending: boolean;
|
|
52
|
+
/** */
|
|
53
|
+
productPrices: ProductPrice[] | undefined;
|
|
49
54
|
/** */
|
|
50
55
|
shippingInventoryData: ProductWarehouseInventoryInformation | undefined;
|
|
51
56
|
/** */
|
|
@@ -48,11 +48,17 @@ export interface Props {
|
|
|
48
48
|
*/
|
|
49
49
|
const ProductInfo = ({ children }: Props) => {
|
|
50
50
|
const [showDimensionError, setShowDimensionError] = useState(false);
|
|
51
|
-
const {
|
|
51
|
+
const {
|
|
52
|
+
masterProduct,
|
|
53
|
+
selectedVariant,
|
|
54
|
+
variantPending,
|
|
55
|
+
allDimensionsSelected,
|
|
56
|
+
} = useProductIntro();
|
|
52
57
|
|
|
53
58
|
const hasVariants = masterProduct?.Dimensions?.length !== 0;
|
|
54
59
|
|
|
55
60
|
const id = hasVariants ? selectedVariant?.RecordId : masterProduct?.RecordId;
|
|
61
|
+
const productId = selectedVariant?.RecordId || masterProduct?.RecordId;
|
|
56
62
|
|
|
57
63
|
const shippingInventorySearchCriteria =
|
|
58
64
|
!hasVariants || selectedVariant
|
|
@@ -65,11 +71,23 @@ const ProductInfo = ({ children }: Props) => {
|
|
|
65
71
|
enabled: !!shippingInventorySearchCriteria,
|
|
66
72
|
});
|
|
67
73
|
|
|
74
|
+
const { data: productPrices, isPending: pricesPending } =
|
|
75
|
+
getProductPrices.useData({
|
|
76
|
+
productIds: [productId],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const isProductDetailsPending =
|
|
80
|
+
pricesPending ||
|
|
81
|
+
!productPrices ||
|
|
82
|
+
variantPending ||
|
|
83
|
+
(allDimensionsSelected && inventoryPending);
|
|
84
|
+
|
|
68
85
|
return (
|
|
69
86
|
<ProductInfoContext.Provider
|
|
70
87
|
value={{
|
|
71
88
|
hasVariants,
|
|
72
|
-
|
|
89
|
+
isProductDetailsPending,
|
|
90
|
+
productPrices,
|
|
73
91
|
shippingInventoryData,
|
|
74
92
|
showDimensionError,
|
|
75
93
|
setShowDimensionError,
|
|
@@ -153,23 +171,13 @@ ProductInfo.Dimensions = () => {
|
|
|
153
171
|
};
|
|
154
172
|
|
|
155
173
|
ProductInfo.Price = () => {
|
|
156
|
-
const IntroCtx = useProductIntro();
|
|
157
|
-
if (!IntroCtx)
|
|
158
|
-
throw new Error(
|
|
159
|
-
"ProductInfo.Inventory must be wrapped in ProductInfoContext and a ProductIntroContext",
|
|
160
|
-
);
|
|
161
|
-
const { masterProduct, selectedVariant } = IntroCtx;
|
|
162
174
|
const { formatCurrency } = useCurrencyFormatter();
|
|
163
|
-
|
|
164
|
-
const { data: productPrices, isPending: pricesPending } =
|
|
165
|
-
getProductPrices.useData({
|
|
166
|
-
productIds: [selectedVariant?.RecordId || masterProduct?.RecordId],
|
|
167
|
-
});
|
|
175
|
+
const { isProductDetailsPending, productPrices } = useProductInfo();
|
|
168
176
|
|
|
169
177
|
return (
|
|
170
178
|
<Box pt={1}>
|
|
171
179
|
<H2 color="primary.main" mb={0.5} lineHeight="1">
|
|
172
|
-
{
|
|
180
|
+
{isProductDetailsPending ? (
|
|
173
181
|
<CircularProgress size={20} />
|
|
174
182
|
) : (
|
|
175
183
|
formatCurrency(productPrices?.[0]?.CustomerContextualPrice || 0)
|
|
@@ -190,8 +198,8 @@ ProductInfo.Inventory = () => {
|
|
|
190
198
|
throw new Error(
|
|
191
199
|
"ProductInfo.Inventory must be wrapped in ProductInfoContext and a ProductIntroContext",
|
|
192
200
|
);
|
|
193
|
-
const {
|
|
194
|
-
const { shippingInventoryData,
|
|
201
|
+
const { allDimensionsSelected } = IntroCtx;
|
|
202
|
+
const { shippingInventoryData, isProductDetailsPending } = InfoCtx;
|
|
195
203
|
if (!allDimensionsSelected)
|
|
196
204
|
return (
|
|
197
205
|
<FlexBox alignItems="center" gap={1}>
|
|
@@ -209,9 +217,7 @@ ProductInfo.Inventory = () => {
|
|
|
209
217
|
<FlexBox alignItems="center" gap={1}>
|
|
210
218
|
{/* INVENTORY */}
|
|
211
219
|
<Box sx={{ my: 3 }}>
|
|
212
|
-
{
|
|
213
|
-
<CircularProgress size={20} />
|
|
214
|
-
) : (
|
|
220
|
+
{!isProductDetailsPending && (
|
|
215
221
|
<Typography variant="body2" color="text.secondary" fontWeight={600}>
|
|
216
222
|
{getInventoryLabel(shippingInventoryData)}
|
|
217
223
|
</Typography>
|
|
@@ -15,6 +15,7 @@ import useProductList from "@evenicanpm/storefront-core/src/pages/product-list/u
|
|
|
15
15
|
import generateBreadcrumbs from "@evenicanpm/storefront-core/src/pages/product-list/utils/generate-breadcrumbs";
|
|
16
16
|
import { searchCategory } from "@evenicanpm/storefront-core/src/pages/product-list/utils/search-for-category";
|
|
17
17
|
import { usePreviousRefiners } from "@evenicanpm/storefront-core/src/pages/product-list/utils/use-previous-refiners";
|
|
18
|
+
import type { ProductSearchResult } from "@msdyn365-commerce/retail-proxy";
|
|
18
19
|
import { Box, CircularProgress, Paper, Typography } from "@mui/material";
|
|
19
20
|
import Grid from "@mui/material/Grid2";
|
|
20
21
|
import { useLocale, useTranslations } from "next-intl";
|
|
@@ -149,7 +150,7 @@ ProductListView.Pagination = function Pager() {
|
|
|
149
150
|
ProductListView.Results = function Results() {
|
|
150
151
|
const t = useTranslations("ProductList");
|
|
151
152
|
const { productData, isLoading, view, productPrices } = useProductList();
|
|
152
|
-
const products = productData?.products ?? [];
|
|
153
|
+
const products = (productData?.products ?? []) as ProductSearchResult[];
|
|
153
154
|
const paginationNode = <ProductListView.Pagination />;
|
|
154
155
|
|
|
155
156
|
if (isLoading) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import type { CmsNavItem } from "@evenicanpm/cms/src/content/navigation/types";
|
|
2
3
|
import type { CategoryHierarchy } from "@evenicanpm/storefront-core/src/lib/category-helpers";
|
|
3
4
|
import { createLink } from "@evenicanpm/storefront-core/src/providers/nav-provider/utils/createLink";
|
|
4
5
|
import { useTranslations } from "next-intl";
|
|
5
6
|
import { createContext, type PropsWithChildren, useState } from "react";
|
|
6
|
-
import type { CmsNavItem } from "@/cms/interfaces/navigation";
|
|
7
7
|
|
|
8
8
|
type NavContextType = {
|
|
9
9
|
cmsNav: CmsNavItem[];
|
package/tsconfig.json
CHANGED
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
"resolveJsonModule": true,
|
|
12
12
|
"outDir": "./dist",
|
|
13
13
|
"jsx": "preserve",
|
|
14
|
-
"
|
|
14
|
+
"baseUrl": ".",
|
|
15
15
|
"paths": {
|
|
16
|
-
"*": ["../../../../storefront/node_modules/*"],
|
|
17
16
|
"@evenicanpm/storefront-core/src/*": ["./src/*"],
|
|
18
17
|
"@evenicanpm/storefront-core/src/lib/*": ["src/lib/*"],
|
|
19
18
|
"@evenicanpm/storefront-core/src/api-manager/*": ["src/api-manager/*"],
|
package/__mocks__/countries.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { CountryRegionInfo } from "@msdyn365-commerce/retail-proxy";
|
|
2
|
-
|
|
3
|
-
export const countries: CountryRegionInfo[] = [
|
|
4
|
-
{ ShortName: "Canada", CountryRegionId: "CA", ISOCode: "CA" },
|
|
5
|
-
{
|
|
6
|
-
ShortName: "United States",
|
|
7
|
-
CountryRegionId: "US",
|
|
8
|
-
ISOCode: "US",
|
|
9
|
-
},
|
|
10
|
-
{ ShortName: "Mexico", CountryRegionId: "MX", ISOCode: "MX" },
|
|
11
|
-
];
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { queryClient } from "./query-client";
|
|
2
|
-
|
|
3
|
-
type MutationInput = {
|
|
4
|
-
cartLine?: Array<{ Quantity?: number }>;
|
|
5
|
-
} & Record<string, unknown>;
|
|
6
|
-
|
|
7
|
-
type CartMockData = {
|
|
8
|
-
CartLines: Array<Record<string, unknown>>;
|
|
9
|
-
} & Record<string, unknown>;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
* Mocking up at the react-query level because our components
|
|
14
|
-
* rely on react-query for triggering re-renders and state subscriptions.
|
|
15
|
-
*
|
|
16
|
-
*/
|
|
17
|
-
export const useCreateMutation = (apiManagerPath: readonly unknown[]) => {
|
|
18
|
-
switch (apiManagerPath[1]) {
|
|
19
|
-
case "updateCartLines":
|
|
20
|
-
return {
|
|
21
|
-
mutate: (input: MutationInput) => {
|
|
22
|
-
console.log(input);
|
|
23
|
-
queryClient.setQueryData(["cart"], (oldData: CartMockData) => ({
|
|
24
|
-
...oldData,
|
|
25
|
-
CartLines: [
|
|
26
|
-
{
|
|
27
|
-
...oldData.CartLines[0],
|
|
28
|
-
Quantity: input.cartLine?.[0]?.Quantity,
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
}));
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
case "deleteCartLine":
|
|
35
|
-
return {
|
|
36
|
-
mutate: (input: MutationInput) => {
|
|
37
|
-
console.log(input);
|
|
38
|
-
queryClient.setQueryData(["cart"], (oldData: CartMockData) => ({
|
|
39
|
-
...oldData,
|
|
40
|
-
CartLines: [],
|
|
41
|
-
}));
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
case "addToCart":
|
|
45
|
-
return {
|
|
46
|
-
mutate: (input: MutationInput) => {
|
|
47
|
-
console.log(input);
|
|
48
|
-
queryClient.setQueryData(["cart"], (oldData: CartMockData) => ({
|
|
49
|
-
...oldData,
|
|
50
|
-
CartLines: [
|
|
51
|
-
{
|
|
52
|
-
ProductId: 1,
|
|
53
|
-
Quantity: 1,
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
}));
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
case "sessionInit":
|
|
60
|
-
return { mutateAsync: () => {} };
|
|
61
|
-
default:
|
|
62
|
-
return {
|
|
63
|
-
mutate: () => {
|
|
64
|
-
console.log("No Mock Implemented");
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
};
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { useQuery } from "@tanstack/react-query";
|
|
2
|
-
import categories from "../__mocks__/data/categories.json";
|
|
3
|
-
import { data as products } from "../__mocks__/products";
|
|
4
|
-
import wishlists from "../__mocks__/wishlists.json";
|
|
5
|
-
|
|
6
|
-
const cartBase = {
|
|
7
|
-
Id: "cart-123",
|
|
8
|
-
Version: 1,
|
|
9
|
-
CartLines: [
|
|
10
|
-
{
|
|
11
|
-
LineId: "line-1",
|
|
12
|
-
ProductId: 1,
|
|
13
|
-
Quantity: 0,
|
|
14
|
-
},
|
|
15
|
-
],
|
|
16
|
-
};
|
|
17
|
-
const cart = cartBase;
|
|
18
|
-
|
|
19
|
-
const shippingInventoryData = {
|
|
20
|
-
AggregatedProductInventoryAvailabilities: [
|
|
21
|
-
{
|
|
22
|
-
TotalAvailableInventoryLevelCode: "AVAIL",
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Need to mock create query as fn() and then
|
|
28
|
-
// mock the return value in the respective storeies
|
|
29
|
-
// files, per component
|
|
30
|
-
export const createQuery = (apiManagerPath: string[]) => {
|
|
31
|
-
console.log(apiManagerPath);
|
|
32
|
-
if (apiManagerPath[0] === "cart")
|
|
33
|
-
return {
|
|
34
|
-
useData: () => useQuery({ queryKey: ["cart"], queryFn: () => cart }),
|
|
35
|
-
};
|
|
36
|
-
if (apiManagerPath[1] === "getInventory") {
|
|
37
|
-
return {
|
|
38
|
-
useData: () =>
|
|
39
|
-
useQuery({
|
|
40
|
-
queryKey: ["cart", "getInventory"],
|
|
41
|
-
queryFn: () => shippingInventoryData,
|
|
42
|
-
}),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (apiManagerPath[1] === "searchProducts") {
|
|
46
|
-
console.log("here are the products");
|
|
47
|
-
console.log(products.products.length);
|
|
48
|
-
return {
|
|
49
|
-
useData: () =>
|
|
50
|
-
useQuery({
|
|
51
|
-
queryKey: ["product", "searchProducts"],
|
|
52
|
-
queryFn: () => Promise.resolve(products.products),
|
|
53
|
-
}),
|
|
54
|
-
useSuspenseData: () =>
|
|
55
|
-
useQuery({
|
|
56
|
-
queryKey: ["product", "searchProducts"],
|
|
57
|
-
queryFn: () => Promise.resolve(products.products),
|
|
58
|
-
}),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
if (apiManagerPath[1] === "getWishlists") {
|
|
62
|
-
return {
|
|
63
|
-
useData: () =>
|
|
64
|
-
useQuery({
|
|
65
|
-
queryKey: ["wishlists"],
|
|
66
|
-
queryFn: () => wishlists,
|
|
67
|
-
}),
|
|
68
|
-
useSuspenseData: () =>
|
|
69
|
-
useQuery({
|
|
70
|
-
queryKey: ["wishlists"],
|
|
71
|
-
queryFn: () => wishlists,
|
|
72
|
-
}),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
if (apiManagerPath[1] === "getCategories") {
|
|
76
|
-
console.log("Here i am!");
|
|
77
|
-
return {
|
|
78
|
-
useData: () =>
|
|
79
|
-
useQuery({
|
|
80
|
-
queryKey: ["categories", "getCategories"],
|
|
81
|
-
queryFn: () => categories,
|
|
82
|
-
}),
|
|
83
|
-
useSuspenseData: () =>
|
|
84
|
-
useQuery({
|
|
85
|
-
queryKey: ["cateogires", "getCategories"],
|
|
86
|
-
queryFn: () => categories,
|
|
87
|
-
}),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export const createApiPath = (key: string, method: string) => {
|
|
93
|
-
return [key, method];
|
|
94
|
-
};
|