@graphcommerce/magento-product 4.7.2 → 4.8.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.
@@ -4,8 +4,7 @@ fragment ProductListItem on ProductInterface @injectable {
4
4
  sku
5
5
  name
6
6
  small_image {
7
- url
8
- label
7
+ ...ProductImage
9
8
  }
10
9
  price_range {
11
10
  minimum_price {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1679](https://github.com/graphcommerce-org/graphcommerce/pull/1679) [`2b5451395`](https://github.com/graphcommerce-org/graphcommerce/commit/2b5451395dc1173de55d18d08968866e561f90ab) Thanks [@paales](https://github.com/paales)! - Move AddProductsToCartSnackbar to inside AddProductsToCartForm and make AddProductsToCartForm configrable via theme.ts
8
+
9
+ - [#1679](https://github.com/graphcommerce-org/graphcommerce/pull/1679) [`e76df6dc3`](https://github.com/graphcommerce-org/graphcommerce/commit/e76df6dc37c11c793a5d008ba36932d17dc23855) Thanks [@paales](https://github.com/paales)! - Added AddProductsToCartFab for a smaller add to cart button
10
+
11
+ - [#1678](https://github.com/graphcommerce-org/graphcommerce/pull/1678) [`78d7d51cb`](https://github.com/graphcommerce-org/graphcommerce/commit/78d7d51cb1551601d3a4756cd1f2157a49ff93b9) Thanks [@Jessevdpoel](https://github.com/Jessevdpoel)! - Changed styling and forwarded breadcrumbprops
12
+
13
+ ### Patch Changes
14
+
15
+ - [#1679](https://github.com/graphcommerce-org/graphcommerce/pull/1679) [`c4ed376e2`](https://github.com/graphcommerce-org/graphcommerce/commit/c4ed376e2c72b16b34704d7d1ca69c074de172ba) Thanks [@paales](https://github.com/paales)! - Support passing children to AddProductsToCartButton instead of Add To Cart
16
+
17
+ - Updated dependencies [[`e76df6dc3`](https://github.com/graphcommerce-org/graphcommerce/commit/e76df6dc37c11c793a5d008ba36932d17dc23855), [`0bd9ea582`](https://github.com/graphcommerce-org/graphcommerce/commit/0bd9ea58230dde79c5fe2cdb07e9860151460270)]:
18
+ - @graphcommerce/next-ui@4.29.0
19
+ - @graphcommerce/ecommerce-ui@1.5.5
20
+ - @graphcommerce/framer-scroller@2.1.42
21
+ - @graphcommerce/magento-cart@4.9.1
22
+ - @graphcommerce/magento-store@4.3.3
23
+
24
+ ## 4.7.3
25
+
26
+ ### Patch Changes
27
+
28
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`1b1504c9b`](https://github.com/graphcommerce-org/graphcommerce/commit/1b1504c9b0e51f2787bce91e1ff1940f540411d6) Thanks [@paales](https://github.com/paales)! - Added crosssel functionality
29
+
30
+ - Updated dependencies [[`9e630670f`](https://github.com/graphcommerce-org/graphcommerce/commit/9e630670ff6c952ab7b938d890b5509804985cf3), [`cf3518499`](https://github.com/graphcommerce-org/graphcommerce/commit/cf351849999ad6fe73ce2bb258098a7dd301d517), [`81f31d1e5`](https://github.com/graphcommerce-org/graphcommerce/commit/81f31d1e54397368088a4289aaddd29facfceeef), [`2e9fa5984`](https://github.com/graphcommerce-org/graphcommerce/commit/2e9fa5984a07ff14fc1b3a4f62189a26e8e3ecdd), [`adf13069a`](https://github.com/graphcommerce-org/graphcommerce/commit/adf13069af6460c960276b402237371c12fc6dec), [`a8905d263`](https://github.com/graphcommerce-org/graphcommerce/commit/a8905d263273cb9322583d5759a5fdc66eceb8e4), [`1b1504c9b`](https://github.com/graphcommerce-org/graphcommerce/commit/1b1504c9b0e51f2787bce91e1ff1940f540411d6), [`8a34f8081`](https://github.com/graphcommerce-org/graphcommerce/commit/8a34f808186274a6fe1d4f309472f1a9c6d00efd), [`3dde492ad`](https://github.com/graphcommerce-org/graphcommerce/commit/3dde492ad3a49d96481eeb7453fb305d0017b1a5)]:
31
+ - @graphcommerce/next-ui@4.28.1
32
+ - @graphcommerce/graphql@3.5.0
33
+ - @graphcommerce/framer-scroller@2.1.41
34
+ - @graphcommerce/magento-cart@4.9.0
35
+ - @graphcommerce/ecommerce-ui@1.5.4
36
+ - @graphcommerce/magento-store@4.3.2
37
+ - @graphcommerce/framer-next-pages@3.3.2
38
+ - @graphcommerce/image@3.1.10
39
+
3
40
  ## 4.7.2
4
41
 
5
42
  ### Patch Changes
@@ -1,48 +1,31 @@
1
1
  import { Button, ButtonProps } from '@graphcommerce/next-ui'
2
2
  import { Trans } from '@lingui/react'
3
- import { SxProps, Theme, useEventCallback } from '@mui/material'
4
- import { useFormAddProductsToCart } from './AddProductsToCartForm'
3
+ import {
4
+ useAddProductsToCartAction,
5
+ UseAddProductsToCartActionProps,
6
+ } from './useAddProductsToCartAction'
5
7
 
6
- export type AddProductsToCartButtonProps = {
7
- sx?: SxProps<Theme>
8
- sku: string
9
- index?: number
10
- } & Pick<
11
- ButtonProps<'button'>,
12
- | 'variant'
13
- | 'color'
14
- | 'size'
15
- | 'disabled'
16
- | 'fullWidth'
17
- | 'startIcon'
18
- | 'endIcon'
19
- | 'onClick'
20
- | 'loading'
21
- >
8
+ export type AddProductsToCartButtonProps = UseAddProductsToCartActionProps &
9
+ Pick<
10
+ ButtonProps<'button'>,
11
+ | 'variant'
12
+ | 'color'
13
+ | 'size'
14
+ | 'fullWidth'
15
+ | 'startIcon'
16
+ | 'endIcon'
17
+ | 'onClick'
18
+ | 'sx'
19
+ | 'children'
20
+ >
22
21
 
23
22
  export function AddProductsToCartButton(props: AddProductsToCartButtonProps) {
24
- const { formState, setValue } = useFormAddProductsToCart()
25
- const { loading, sku, index = 0, disabled, onClick } = props
26
-
27
- const clickHandler: NonNullable<AddProductsToCartButtonProps['onClick']> = useEventCallback(
28
- (e) => {
29
- setValue(`cartItems.${index}.sku`, sku)
30
- onClick?.(e)
31
- },
32
- )
23
+ const { children } = props
24
+ const action = useAddProductsToCartAction(props)
33
25
 
34
26
  return (
35
- <Button
36
- type='submit'
37
- color='primary'
38
- variant='pill'
39
- size='large'
40
- {...props}
41
- disabled={Boolean(formState.errors.cartItems?.[index].sku?.message) || disabled}
42
- loading={formState.isSubmitting || loading}
43
- onClick={clickHandler}
44
- >
45
- <Trans id='Add to Cart' />
27
+ <Button type='submit' color='primary' variant='pill' size='large' {...props} {...action}>
28
+ {children || <Trans id='Add to Cart' />}
46
29
  </Button>
47
30
  )
48
31
  }
@@ -1,5 +1,5 @@
1
1
  import { FormHelperText } from '@mui/material'
2
- import { useFormAddProductsToCart } from './AddProductsToCartForm'
2
+ import { useFormAddProductsToCart } from './useFormAddProductsToCart'
3
3
 
4
4
  type AddProductsToCartErrorProps = {
5
5
  children?: React.ReactNode
@@ -0,0 +1,18 @@
1
+ import { Fab, FabProps, iconShoppingBag } from '@graphcommerce/next-ui'
2
+ import { SxProps, Theme } from '@mui/material'
3
+ import {
4
+ useAddProductsToCartAction,
5
+ UseAddProductsToCartActionProps,
6
+ } from './useAddProductsToCartAction'
7
+
8
+ export type AddProductsToCartFabProps = {
9
+ sx?: SxProps<Theme>
10
+ icon?: FabProps['icon']
11
+ } & Pick<FabProps, 'color' | 'size'> &
12
+ UseAddProductsToCartActionProps
13
+
14
+ export function AddProductsToCartFab(props: AddProductsToCartFabProps) {
15
+ const { icon = iconShoppingBag } = props
16
+ const action = useAddProductsToCartAction(props)
17
+ return <Fab type='submit' {...props} {...action} icon={icon} />
18
+ }
@@ -1,69 +1,111 @@
1
- import { UseFormGqlMutationReturn, UseFormGraphQlOptions } from '@graphcommerce/ecommerce-ui'
1
+ import { UseFormGraphQlOptions } from '@graphcommerce/ecommerce-ui'
2
2
  import { useFormGqlMutationCart } from '@graphcommerce/magento-cart'
3
- import { Box, SxProps, Theme } from '@mui/material'
4
- import { createContext, useContext, useMemo } from 'react'
3
+ import { ExtendableComponent } from '@graphcommerce/next-ui'
4
+ import { Box, SxProps, Theme, useThemeProps } from '@mui/material'
5
+ import { useRouter } from 'next/router'
6
+ import { useMemo } from 'react'
5
7
  import {
6
8
  AddProductsToCartDocument,
7
9
  AddProductsToCartMutation,
8
10
  AddProductsToCartMutationVariables,
9
11
  } from './AddProductsToCart.gql'
10
-
11
- type AddProductsToCartContextType = UseFormGqlMutationReturn<
12
- AddProductsToCartMutation,
13
- AddProductsToCartMutationVariables
14
- >
15
-
16
- export const addProductsToCartContext = createContext(
17
- undefined as AddProductsToCartContextType | undefined,
18
- )
12
+ import {
13
+ AddProductsToCartSnackbar,
14
+ AddProductsToCartSnackbarProps,
15
+ } from './AddProductsToCartSnackbar'
16
+ import { AddProductsToCartContext, RedirectType } from './useFormAddProductsToCart'
19
17
 
20
18
  type AddProductsToCartFormProps = {
19
+ // The props are actually used, but are passed through useThemeProps and that breaks react/no-unused-prop-types
20
+ // eslint-disable-next-line react/no-unused-prop-types
21
21
  children: React.ReactNode
22
+ // eslint-disable-next-line react/no-unused-prop-types
22
23
  sx?: SxProps<Theme>
23
- } & Omit<
24
- UseFormGraphQlOptions<AddProductsToCartMutation, AddProductsToCartMutationVariables>,
25
- 'onBeforeSubmit'
26
- >
24
+ // eslint-disable-next-line react/no-unused-prop-types
25
+ redirect?: RedirectType
26
+ } & UseFormGraphQlOptions<AddProductsToCartMutation, AddProductsToCartMutationVariables> &
27
+ AddProductsToCartSnackbarProps
28
+
29
+ const name = 'AddProductsToCartForm'
27
30
 
31
+ /** Expose the component to be exendable in your theme.components */
32
+ declare module '@mui/material/styles/components' {
33
+ interface Components {
34
+ AddProductsToCartForm?: Pick<
35
+ ExtendableComponent<Omit<AddProductsToCartFormProps, 'children'>>,
36
+ 'defaultProps'
37
+ >
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Component that handles adding products to the cart. Used on the product page, but can be used for
43
+ * any product listing.
44
+ *
45
+ * Can be configured globally in your theme.ts;
46
+ *
47
+ * - Uses react-hook-form's useForm hook under the hood and exposes the form as a context which can be
48
+ * consumed with `useFormAddProductsToCart` hook.
49
+ * - Cleans up the submitted data.
50
+ * - Redirects the user to the cart/checkout/added page after successful submission.
51
+ */
28
52
  export function AddProductsToCartForm(props: AddProductsToCartFormProps) {
29
- const { children, defaultValues, sx, ...formProps } = props
30
- const form = useFormGqlMutationCart(AddProductsToCartDocument, {
31
- defaultValues,
53
+ const {
54
+ children,
55
+ redirect = 'cart',
56
+ onComplete,
57
+ sx,
58
+ errorSnackbar,
59
+ successSnackbar,
60
+ ...formProps
61
+ } = useThemeProps({ name, props })
62
+ const router = useRouter()
32
63
 
33
- // We're stripping out incomplete entered options.
34
- onBeforeSubmit: ({ cartId, cartItems }) => ({
35
- cartId,
36
- cartItems: cartItems
37
- .filter((cartItem) => cartItem.sku)
38
- .map((cartItem) => ({
39
- ...cartItem,
40
- selected_options: cartItem.selected_options?.filter(Boolean),
41
- entered_options: cartItem.entered_options?.filter((option) => option?.value),
42
- })),
43
- }),
64
+ const form = useFormGqlMutationCart<
65
+ AddProductsToCartMutation,
66
+ AddProductsToCartMutationVariables
67
+ >(AddProductsToCartDocument, {
44
68
  ...formProps,
69
+ // We're stripping out incomplete entered options.
70
+ onBeforeSubmit: async (variables) => {
71
+ const variables2 = (await formProps.onBeforeSubmit?.(variables)) ?? variables
72
+ if (variables2 === false) return false
73
+
74
+ const { cartId, cartItems } = variables2
75
+ return {
76
+ cartId,
77
+ cartItems: cartItems
78
+ .filter((cartItem) => cartItem.sku)
79
+ .map((cartItem) => ({
80
+ ...cartItem,
81
+ quantity: cartItem.quantity || 1,
82
+ selected_options: cartItem.selected_options?.filter(Boolean),
83
+ entered_options: cartItem.entered_options?.filter((option) => option?.value),
84
+ })),
85
+ }
86
+ },
87
+ onComplete: async (result, variables) => {
88
+ await onComplete?.(result, variables)
89
+
90
+ if (result.data?.addProductsToCart?.user_errors?.length || result.errors?.length || !redirect)
91
+ return
92
+
93
+ if (redirect === 'checkout') await router.push('/checkout')
94
+ if (redirect === 'added') await router.push({ pathname: '/checkout/added' })
95
+ if (redirect === 'cart') await router.push({ pathname: '/cart' })
96
+ },
45
97
  })
46
98
 
47
99
  const submit = form.handleSubmit(() => {})
48
100
 
49
101
  return (
50
- <addProductsToCartContext.Provider value={form}>
51
- <Box component='form' onSubmit={submit} noValidate sx={sx}>
102
+ <AddProductsToCartContext.Provider
103
+ value={useMemo(() => ({ ...form, redirect }), [form, redirect])}
104
+ >
105
+ <Box component='form' onSubmit={submit} noValidate sx={sx} className={name}>
52
106
  {children}
53
107
  </Box>
54
- </addProductsToCartContext.Provider>
108
+ <AddProductsToCartSnackbar errorSnackbar={errorSnackbar} successSnackbar={successSnackbar} />
109
+ </AddProductsToCartContext.Provider>
55
110
  )
56
111
  }
57
-
58
- export function useFormAddProductsToCart(optional: true): AddProductsToCartContextType | undefined
59
- export function useFormAddProductsToCart(optional?: false): AddProductsToCartContextType
60
- export function useFormAddProductsToCart(optional = false) {
61
- const context = useContext(addProductsToCartContext)
62
-
63
- if (!optional && typeof context === 'undefined') {
64
- throw Error(
65
- 'useFormAddProductsToCart must be used within a AddProductsToCartForm or provide the optional=true argument',
66
- )
67
- }
68
- return context
69
- }
@@ -1,5 +1,5 @@
1
1
  import { NumberFieldElement, NumberFieldElementProps } from '@graphcommerce/ecommerce-ui'
2
- import { useFormAddProductsToCart } from './AddProductsToCartForm'
2
+ import { useFormAddProductsToCart } from './useFormAddProductsToCart'
3
3
 
4
4
  type AddToCartQuantityProps = Omit<
5
5
  NumberFieldElementProps,
@@ -1,67 +1,84 @@
1
1
  import { ApolloCartErrorSnackbar } from '@graphcommerce/magento-cart'
2
2
  import {
3
3
  Button,
4
+ ErrorSnackbar,
5
+ ErrorSnackbarProps,
6
+ filterNonNullableKeys,
4
7
  iconChevronRight,
5
8
  IconSvg,
6
9
  MessageSnackbar,
7
- ErrorSnackbar,
10
+ MessageSnackbarProps,
8
11
  } from '@graphcommerce/next-ui'
9
12
  import { Trans } from '@lingui/react'
10
13
  import PageLink from 'next/link'
11
- import { useFormAddProductsToCart } from './AddProductsToCartForm'
14
+ import { useFormAddProductsToCart } from './useFormAddProductsToCart'
15
+
16
+ export type AddProductsToCartSnackbarProps = {
17
+ errorSnackbar?: Omit<ErrorSnackbarProps, 'open'>
18
+ successSnackbar?: Omit<MessageSnackbarProps, 'open' | 'action'>
19
+ }
12
20
 
13
- type AddToCartMessageProps = { name?: string | null }
21
+ export function AddProductsToCartSnackbar(props: AddProductsToCartSnackbarProps) {
22
+ const { errorSnackbar, successSnackbar } = props
23
+ const { formState, error, data, redirect } = useFormAddProductsToCart()
14
24
 
15
- export function AddProductsToCartSnackbar(props: AddToCartMessageProps) {
16
- const { name } = props
17
- const { formState, error, data } = useFormAddProductsToCart()
25
+ const showSuccess =
26
+ !formState.isSubmitting &&
27
+ formState.isSubmitSuccessful &&
28
+ !error?.message &&
29
+ !data?.addProductsToCart?.user_errors?.length &&
30
+ !redirect
18
31
 
32
+ const items = filterNonNullableKeys(data?.addProductsToCart?.cart.items)
33
+
34
+ const showErrorSnackbar = (data?.addProductsToCart?.user_errors?.length ?? 0) > 0
19
35
  return (
20
36
  <>
21
- <ApolloCartErrorSnackbar error={error} />
22
-
23
- <ErrorSnackbar
24
- variant='pill'
25
- severity='error'
26
- open={(data?.addProductsToCart?.user_errors?.length ?? 0) > 0}
27
- action={
28
- <Button size='medium' variant='pill' color='secondary'>
29
- <Trans id='Ok' />
30
- </Button>
31
- }
32
- >
33
- <>{data?.addProductsToCart?.user_errors?.map((e) => e?.message).join(', ')}</>
34
- </ErrorSnackbar>
37
+ {error && <ApolloCartErrorSnackbar error={error} />}
35
38
 
36
- <MessageSnackbar
37
- open={
38
- !formState.isSubmitting &&
39
- formState.isSubmitSuccessful &&
40
- !error?.message &&
41
- !data?.addProductsToCart?.user_errors?.length
42
- }
43
- variant='pill'
44
- action={
45
- <PageLink href='/cart' passHref>
46
- <Button
47
- id='view-shopping-cart-button'
48
- size='medium'
49
- variant='pill'
50
- color='secondary'
51
- endIcon={<IconSvg src={iconChevronRight} />}
52
- sx={{ display: 'flex' }}
53
- >
54
- <Trans id='View shopping cart' />
39
+ {showErrorSnackbar && (
40
+ <ErrorSnackbar
41
+ variant='pill'
42
+ severity='error'
43
+ action={
44
+ <Button size='medium' variant='pill' color='secondary'>
45
+ <Trans id='Ok' />
55
46
  </Button>
56
- </PageLink>
57
- }
58
- >
59
- <Trans
60
- id='<0>{name}</0> has been added to your shopping cart!'
61
- components={{ 0: <strong /> }}
62
- values={{ name }}
63
- />
64
- </MessageSnackbar>
47
+ }
48
+ {...errorSnackbar}
49
+ open={showErrorSnackbar}
50
+ >
51
+ <>{data?.addProductsToCart?.user_errors?.map((e) => e?.message).join(', ')}</>
52
+ </ErrorSnackbar>
53
+ )}
54
+
55
+ {showSuccess && (
56
+ <MessageSnackbar
57
+ variant='pill'
58
+ {...successSnackbar}
59
+ open={showSuccess}
60
+ action={
61
+ <PageLink href='/cart' passHref>
62
+ <Button
63
+ id='view-shopping-cart-button'
64
+ size='medium'
65
+ variant='pill'
66
+ color='secondary'
67
+ endIcon={<IconSvg src={iconChevronRight} />}
68
+ sx={{ display: 'flex' }}
69
+ >
70
+ <Trans id='View shopping cart' />
71
+ </Button>
72
+ </PageLink>
73
+ }
74
+ >
75
+ <Trans
76
+ id='<0>{name}</0> has been added to your shopping cart!'
77
+ components={{ 0: <strong /> }}
78
+ values={{ name: items[items.length - 1]?.product.name }}
79
+ />
80
+ </MessageSnackbar>
81
+ )}
65
82
  </>
66
83
  )
67
84
  }
@@ -3,4 +3,5 @@ export * from './AddProductsToCartButton'
3
3
  export * from './AddProductsToCartError'
4
4
  export * from './AddProductsToCartForm'
5
5
  export * from './AddProductsToCartQuantity'
6
- export * from './AddProductsToCartSnackbar'
6
+ export * from './useFormAddProductsToCart'
7
+ export * from './AddProductsToCartFab'
@@ -0,0 +1,40 @@
1
+ import { useEventCallback } from '@mui/material'
2
+ import { useMemo } from 'react'
3
+ import { useFormAddProductsToCart } from './useFormAddProductsToCart'
4
+
5
+ export type UseAddProductsToCartActionProps = {
6
+ sku: string | null | undefined
7
+ index?: number
8
+ disabled?: boolean
9
+ loading?: boolean
10
+ onClick?: React.MouseEventHandler<HTMLButtonElement>
11
+ }
12
+
13
+ export type UseAddProductsToCartActionReturn = {
14
+ disabled: boolean
15
+ loading: boolean
16
+ onClick: React.MouseEventHandler<HTMLButtonElement>
17
+ onMouseDown: React.MouseEventHandler<HTMLButtonElement>
18
+ }
19
+
20
+ export function useAddProductsToCartAction(
21
+ props: UseAddProductsToCartActionProps,
22
+ ): UseAddProductsToCartActionReturn {
23
+ const { formState, setValue, getValues } = useFormAddProductsToCart()
24
+ const { sku, index = 0, onClick: onClickIncomming, disabled, loading } = props
25
+
26
+ return {
27
+ disabled: Boolean(formState.errors.cartItems?.[index].sku?.message || disabled),
28
+ loading: loading || (formState.isSubmitting && getValues(`cartItems.${index}.sku`) === sku),
29
+ onClick: useEventCallback((e) => {
30
+ e.stopPropagation()
31
+ if (formState.isSubmitting) return
32
+ if (process.env.NODE_ENV !== 'production') {
33
+ if (!sku) console.warn(`You must provide a 'sku' to useAddProductsToCartAction`)
34
+ }
35
+ setValue(`cartItems.${index}.sku`, sku ?? '')
36
+ onClickIncomming?.(e)
37
+ }),
38
+ onMouseDown: useEventCallback((e) => e.stopPropagation()),
39
+ }
40
+ }
@@ -0,0 +1,30 @@
1
+ import { UseFormGqlMutationReturn } from '@graphcommerce/ecommerce-ui'
2
+ import { createContext, useContext } from 'react'
3
+ import {
4
+ AddProductsToCartMutation,
5
+ AddProductsToCartMutationVariables,
6
+ } from './AddProductsToCart.gql'
7
+
8
+ export type RedirectType = 'added' | 'cart' | 'checkout' | undefined
9
+
10
+ export type AddProductsToCartContextType = { redirect: RedirectType } & UseFormGqlMutationReturn<
11
+ AddProductsToCartMutation,
12
+ AddProductsToCartMutationVariables
13
+ >
14
+
15
+ export const AddProductsToCartContext = createContext(
16
+ undefined as AddProductsToCartContextType | undefined,
17
+ )
18
+
19
+ export function useFormAddProductsToCart(optional: true): AddProductsToCartContextType | undefined
20
+ export function useFormAddProductsToCart(optional?: false): AddProductsToCartContextType
21
+ export function useFormAddProductsToCart(optional = false) {
22
+ const context = useContext(AddProductsToCartContext)
23
+
24
+ if (!optional && typeof context === 'undefined') {
25
+ throw Error(
26
+ 'useFormAddProductsToCart must be used within a AddProductsToCartForm or provide the optional=true argument',
27
+ )
28
+ }
29
+ return context
30
+ }
@@ -1,5 +1,6 @@
1
1
  import { SelectElement, TextFieldElement } from '@graphcommerce/ecommerce-ui'
2
2
  import { filterNonNullableKeys, RenderType, TypeRenderer } from '@graphcommerce/next-ui'
3
+ import React from 'react'
3
4
  import { useFormAddProductsToCart } from '../AddProductsToCart'
4
5
  import { ProductCustomizableFragment } from './ProductCustomizable.gql'
5
6
 
@@ -10,7 +11,9 @@ type OptionTypeRenderer = TypeRenderer<
10
11
  }
11
12
  >
12
13
 
13
- const CustomizableAreaOption: OptionTypeRenderer['CustomizableAreaOption'] = (props) => {
14
+ const CustomizableAreaOption = React.memo<
15
+ React.ComponentProps<OptionTypeRenderer['CustomizableAreaOption']>
16
+ >((props) => {
14
17
  const { uid, areaValue, required, optionIndex, index, title } = props
15
18
  const maxLength = areaValue?.max_characters ?? undefined
16
19
  const { control, register } = useFormAddProductsToCart()
@@ -34,9 +37,11 @@ const CustomizableAreaOption: OptionTypeRenderer['CustomizableAreaOption'] = (pr
34
37
  />
35
38
  </>
36
39
  )
37
- }
40
+ })
38
41
 
39
- const CustomizableDropDownOption: OptionTypeRenderer['CustomizableDropDownOption'] = (props) => {
42
+ const CustomizableDropDownOption = React.memo<
43
+ React.ComponentProps<OptionTypeRenderer['CustomizableDropDownOption']>
44
+ >((props) => {
40
45
  const { uid, required, optionIndex, index, title, dropdownValue } = props
41
46
  const { control, register } = useFormAddProductsToCart()
42
47
 
@@ -52,6 +57,7 @@ const CustomizableDropDownOption: OptionTypeRenderer['CustomizableDropDownOption
52
57
  name={`cartItems.${index}.entered_options.${optionIndex}.value`}
53
58
  label={title}
54
59
  required={Boolean(required)}
60
+ defaultValue=''
55
61
  options={filterNonNullableKeys(dropdownValue, ['title']).map((option) => ({
56
62
  id: option.uid,
57
63
  label: option.title,
@@ -59,7 +65,7 @@ const CustomizableDropDownOption: OptionTypeRenderer['CustomizableDropDownOption
59
65
  />
60
66
  </>
61
67
  )
62
- }
68
+ })
63
69
 
64
70
  const renderer: OptionTypeRenderer = {
65
71
  CustomizableAreaOption,
@@ -84,7 +90,7 @@ export function ProductCustomizable(props: ProductCustomizableProps) {
84
90
  key={option.uid}
85
91
  renderer={renderer}
86
92
  {...option}
87
- optionIndex={option.sort_order - 1}
93
+ optionIndex={option.sort_order + 100}
88
94
  index={index}
89
95
  />
90
96
  ))}
@@ -6,7 +6,16 @@ import {
6
6
  breakpointVal,
7
7
  } from '@graphcommerce/next-ui'
8
8
  import { Trans } from '@lingui/react'
9
- import { ButtonBase, Typography, Box, styled, SxProps, Theme } from '@mui/material'
9
+ import {
10
+ ButtonBase,
11
+ Typography,
12
+ Box,
13
+ styled,
14
+ SxProps,
15
+ Theme,
16
+ ButtonBaseProps,
17
+ useEventCallback,
18
+ } from '@mui/material'
10
19
  import PageLink from 'next/link'
11
20
  import React, { PropsWithChildren } from 'react'
12
21
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
@@ -40,16 +49,15 @@ type StyleProps = {
40
49
  imageOnly?: boolean
41
50
  }
42
51
 
43
- type BaseProps = PropsWithChildren<
44
- { subTitle?: React.ReactNode } & StyleProps &
45
- OverlayAreas &
46
- ProductListItemFragment &
47
- Pick<ImageProps, 'loading' | 'sizes' | 'dontReportWronglySizedImages'>
48
- >
52
+ type BaseProps = { subTitle?: React.ReactNode; children?: React.ReactNode } & StyleProps &
53
+ OverlayAreas &
54
+ ProductListItemFragment &
55
+ Pick<ImageProps, 'loading' | 'sizes' | 'dontReportWronglySizedImages'>
49
56
 
50
57
  export type ProductListItemProps = BaseProps & {
51
58
  sx?: SxProps<Theme>
52
59
  titleComponent?: React.ElementType
60
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>, item: ProductListItemFragment) => void
53
61
  }
54
62
 
55
63
  const StyledImage = styled(Image)({})
@@ -72,8 +80,13 @@ export function ProductListItem(props: ProductListItemProps) {
72
80
  aspectRatio = [4, 3],
73
81
  titleComponent = 'h2',
74
82
  sx = [],
83
+ onClick,
75
84
  } = props
76
85
 
86
+ const handleClick = useEventCallback((e: React.MouseEvent<HTMLAnchorElement>) =>
87
+ onClick?.(e, props),
88
+ )
89
+
77
90
  const productLink = useProductLink(props)
78
91
  const discount = Math.floor(price_range.minimum_price.discount?.percent_off ?? 0)
79
92
 
@@ -98,6 +111,7 @@ export function ProductListItem(props: ProductListItemProps) {
98
111
  ...(Array.isArray(sx) ? sx : [sx]),
99
112
  ]}
100
113
  className={classes.root}
114
+ onClick={onClick ? handleClick : undefined}
101
115
  >
102
116
  <Box
103
117
  sx={(theme) => ({
@@ -12,12 +12,19 @@ export type ProductItemsGridProps = {
12
12
  renderers: ProductListItemRenderer
13
13
  loadingEager?: number
14
14
  size?: 'normal' | 'small'
15
- titleComponent?: React.ElementType
16
15
  sx?: BoxProps['sx']
17
- }
16
+ } & Pick<ProductListItemProps, 'onClick' | 'titleComponent'>
18
17
 
19
18
  export function ProductListItemsBase(props: ProductItemsGridProps) {
20
- const { items, sx = [], renderers, loadingEager = 0, size = 'normal', titleComponent } = props
19
+ const {
20
+ items,
21
+ sx = [],
22
+ renderers,
23
+ loadingEager = 0,
24
+ size = 'normal',
25
+ titleComponent,
26
+ onClick,
27
+ } = props
21
28
 
22
29
  return (
23
30
  <Box
@@ -41,14 +48,15 @@ export function ProductListItemsBase(props: ProductItemsGridProps) {
41
48
  <RenderType
42
49
  key={item.uid ?? ''}
43
50
  renderer={renderers}
44
- {...item}
45
- loading={loadingEager > idx ? 'eager' : 'lazy'}
46
51
  sizes={
47
52
  size === 'small'
48
53
  ? { 0: '100vw', 354: '50vw', 675: '30vw', 1255: '23vw', 1500: '337px' }
49
54
  : { 0: '100vw', 367: '48vw', 994: '30vw', 1590: '23vw', 1920: '443px' }
50
55
  }
56
+ {...item}
57
+ loading={loadingEager > idx ? 'eager' : 'lazy'}
51
58
  titleComponent={titleComponent}
59
+ onClick={onClick}
52
60
  noReport
53
61
  />
54
62
  ) : null,
@@ -5,38 +5,36 @@ import PageLink from 'next/link'
5
5
  import { productPageCategory } from '../ProductPageCategory/productPageCategory'
6
6
  import { ProductPageBreadcrumbFragment } from './ProductPageBreadcrumb.gql'
7
7
 
8
- type ProductPageBreadcrumbsProps = ProductPageBreadcrumbFragment & BreadcrumbsProps
8
+ type ProductPageBreadcrumbsProps = ProductPageBreadcrumbFragment & Omit<BreadcrumbsProps, 'children'>
9
9
 
10
10
  export function ProductPageBreadcrumb(props: ProductPageBreadcrumbsProps) {
11
- const { categories, name } = props
11
+ const { categories, name, ...breadcrumbProps } = props
12
12
  const prev = usePrevPageRouter()
13
13
 
14
14
  const category =
15
15
  categories?.find((c) => `/${c?.url_path}` === prev?.asPath) ?? productPageCategory(props)
16
16
 
17
17
  return (
18
- <Container maxWidth={false}>
19
- <Breadcrumbs>
20
- <PageLink href='/' passHref>
21
- <Link underline='hover' color='inherit'>
22
- <Trans id='Home' />
23
- </Link>
24
- </PageLink>
25
- {category?.breadcrumbs?.map((mapped_category, i) => (
26
- <Link
27
- underline='hover'
28
- key={mapped_category?.category_uid}
29
- color='inherit'
30
- href={`/${mapped_category?.category_url_path}`}
31
- >
32
- {mapped_category?.category_name}
33
- </Link>
34
- ))}
35
- <Link underline='hover' color='inherit' href={`/${category?.url_path}`}>
36
- {category?.name}
18
+ <Breadcrumbs {...breadcrumbProps}>
19
+ <PageLink href='/' passHref>
20
+ <Link underline='hover' color='inherit'>
21
+ <Trans id='Home' />
37
22
  </Link>
38
- <Typography color='text.primary'>{name}</Typography>
39
- </Breadcrumbs>
40
- </Container>
23
+ </PageLink>
24
+ {category?.breadcrumbs?.map((mapped_category, i) => (
25
+ <Link
26
+ underline='hover'
27
+ key={mapped_category?.category_uid}
28
+ color='inherit'
29
+ href={`/${mapped_category?.category_url_path}`}
30
+ >
31
+ {mapped_category?.category_name}
32
+ </Link>
33
+ ))}
34
+ <Link underline='hover' color='inherit' href={`/${category?.url_path}`}>
35
+ {category?.name}
36
+ </Link>
37
+ <Typography color='text.primary'>{name}</Typography>
38
+ </Breadcrumbs>
41
39
  )
42
40
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-product",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "4.7.2",
5
+ "version": "4.8.0",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -19,15 +19,15 @@
19
19
  "type-fest": "^2.12.2"
20
20
  },
21
21
  "dependencies": {
22
- "@graphcommerce/ecommerce-ui": "1.5.3",
23
- "@graphcommerce/framer-scroller": "2.1.40",
24
- "@graphcommerce/framer-next-pages": "3.3.1",
25
- "@graphcommerce/graphql": "3.4.8",
22
+ "@graphcommerce/ecommerce-ui": "1.5.5",
23
+ "@graphcommerce/framer-scroller": "2.1.42",
24
+ "@graphcommerce/framer-next-pages": "3.3.2",
25
+ "@graphcommerce/graphql": "3.5.0",
26
26
  "@graphcommerce/graphql-mesh": "4.2.0",
27
- "@graphcommerce/image": "3.1.9",
28
- "@graphcommerce/magento-cart": "4.8.7",
29
- "@graphcommerce/magento-store": "4.3.1",
30
- "@graphcommerce/next-ui": "4.28.0",
27
+ "@graphcommerce/image": "3.1.10",
28
+ "@graphcommerce/magento-cart": "4.9.1",
29
+ "@graphcommerce/magento-store": "4.3.3",
30
+ "@graphcommerce/next-ui": "4.29.0",
31
31
  "schema-dts": "^1.1.0"
32
32
  },
33
33
  "peerDependencies": {