@graphcommerce/magento-product 4.5.10 → 4.6.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1642](https://github.com/graphcommerce-org/graphcommerce/pull/1642) [`ad63ebf4e`](https://github.com/graphcommerce-org/graphcommerce/commit/ad63ebf4e33bfb0e5c9e5e68ab69b14775f3f8a8) Thanks [@paales](https://github.com/paales)! - Introduced `<AddProductsToCartForm/>`, which is allows for adding all product types to the cart with a single react-hook-form form.
8
+
9
+ Which allows you to fully compose the form on the product page without having to modify the page.
10
+
11
+ ### Patch Changes
12
+
13
+ - [#1642](https://github.com/graphcommerce-org/graphcommerce/pull/1642) [`b6bf2c941`](https://github.com/graphcommerce-org/graphcommerce/commit/b6bf2c94197ddacbf8f1fc0d352cd0d46e096f30) Thanks [@paales](https://github.com/paales)! - Remove unused ProductCustomizable on the old product pages
14
+
15
+ - Updated dependencies [[`ad63ebf4e`](https://github.com/graphcommerce-org/graphcommerce/commit/ad63ebf4e33bfb0e5c9e5e68ab69b14775f3f8a8), [`9e6fd498e`](https://github.com/graphcommerce-org/graphcommerce/commit/9e6fd498e3242ab30602767ae77a8e22f80d9fd3)]:
16
+ - @graphcommerce/ecommerce-ui@1.5.0
17
+ - @graphcommerce/magento-store@4.3.0
18
+ - @graphcommerce/next-ui@4.27.0
19
+ - @graphcommerce/magento-cart@4.8.4
20
+ - @graphcommerce/framer-scroller@2.1.39
21
+
3
22
  ## 4.5.10
4
23
 
5
24
  ### Patch Changes
@@ -0,0 +1,13 @@
1
+ mutation AddProductsToCart($cartId: String!, $cartItems: [CartItemInput!]!) {
2
+ addProductsToCart(cartId: $cartId, cartItems: $cartItems) {
3
+ cart {
4
+ id
5
+ __typename
6
+ ...CartItemCountChanged
7
+ }
8
+ user_errors {
9
+ code
10
+ message
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,41 @@
1
+ import { Button, ButtonProps } from '@graphcommerce/next-ui'
2
+ import { Trans } from '@lingui/react'
3
+ import { SxProps, Theme } from '@mui/material'
4
+ import { useFormAddProductsToCart } from './AddProductsToCartForm'
5
+
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
+ >
22
+
23
+ export function AddProductsToCartButton(props: AddProductsToCartButtonProps) {
24
+ const { formState, setValue } = useFormAddProductsToCart()
25
+ const { loading, sku, index = 0 } = props
26
+
27
+ return (
28
+ <Button
29
+ type='submit'
30
+ color='primary'
31
+ variant='pill'
32
+ size='large'
33
+ {...props}
34
+ disabled={Boolean(formState.errors.cartItems?.[index].sku?.message)}
35
+ loading={formState.isSubmitting || loading}
36
+ onClick={() => setValue(`cartItems.${index}.sku`, sku)}
37
+ >
38
+ <Trans id='Add to Cart' />
39
+ </Button>
40
+ )
41
+ }
@@ -0,0 +1,22 @@
1
+ import { FormHelperText } from '@mui/material'
2
+ import { useFormAddProductsToCart } from './AddProductsToCartForm'
3
+
4
+ type AddProductsToCartErrorProps = {
5
+ children?: React.ReactNode
6
+ index?: number
7
+ }
8
+
9
+ export function AddProductsToCartError(props: AddProductsToCartErrorProps) {
10
+ const { children, index = 0 } = props
11
+ const { formState } = useFormAddProductsToCart()
12
+ const errorMsg = formState.errors.cartItems?.[index].sku?.message
13
+
14
+ if (errorMsg)
15
+ return (
16
+ <FormHelperText error sx={{ mt: 0, lineHeight: '1.3' }}>
17
+ {errorMsg}
18
+ </FormHelperText>
19
+ )
20
+
21
+ return <>{children}</>
22
+ }
@@ -0,0 +1,69 @@
1
+ import { UseFormGqlMutationReturn, UseFormGraphQlOptions } from '@graphcommerce/ecommerce-ui'
2
+ import { useFormGqlMutationCart } from '@graphcommerce/magento-cart'
3
+ import { Box, SxProps, Theme } from '@mui/material'
4
+ import { createContext, useContext, useMemo } from 'react'
5
+ import {
6
+ AddProductsToCartDocument,
7
+ AddProductsToCartMutation,
8
+ AddProductsToCartMutationVariables,
9
+ } 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
+ )
19
+
20
+ type AddProductsToCartFormProps = {
21
+ children: React.ReactNode
22
+ sx?: SxProps<Theme>
23
+ } & Omit<
24
+ UseFormGraphQlOptions<AddProductsToCartMutation, AddProductsToCartMutationVariables>,
25
+ 'onBeforeSubmit'
26
+ >
27
+
28
+ export function AddProductsToCartForm(props: AddProductsToCartFormProps) {
29
+ const { children, defaultValues, sx, ...formProps } = props
30
+ const form = useFormGqlMutationCart(AddProductsToCartDocument, {
31
+ defaultValues,
32
+
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
+ }),
44
+ ...formProps,
45
+ })
46
+
47
+ const submit = form.handleSubmit(() => {})
48
+
49
+ return (
50
+ <addProductsToCartContext.Provider value={form}>
51
+ <Box component='form' onSubmit={submit} noValidate sx={sx}>
52
+ {children}
53
+ </Box>
54
+ </addProductsToCartContext.Provider>
55
+ )
56
+ }
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
+ }
@@ -0,0 +1,3 @@
1
+ type AddProductsToCartIndexProps = { index: number }
2
+
3
+ export function AddProductsToCartIndex() {}
@@ -0,0 +1,26 @@
1
+ import { NumberFieldElement, NumberFieldElementProps } from '@graphcommerce/ecommerce-ui'
2
+ import { useFormAddProductsToCart } from './AddProductsToCartForm'
3
+
4
+ type AddToCartQuantityProps = Omit<
5
+ NumberFieldElementProps,
6
+ 'error' | 'required' | 'inputProps' | 'inputRef' | 'helperText' | 'name'
7
+ > & { index?: number }
8
+
9
+ export function AddProductsToCartQuantity(props: AddToCartQuantityProps) {
10
+ const { index = 0 } = props
11
+ const { formState, control } = useFormAddProductsToCart()
12
+
13
+ return (
14
+ <NumberFieldElement
15
+ variant='outlined'
16
+ size='small'
17
+ {...props}
18
+ required
19
+ inputProps={{ min: 1 }}
20
+ defaultValue='1'
21
+ control={control}
22
+ name={`cartItems.${index}.quantity`}
23
+ helperText={formState.isSubmitted && formState.errors.cartItems?.[index].quantity?.message}
24
+ />
25
+ )
26
+ }
@@ -0,0 +1,67 @@
1
+ import { ApolloCartErrorSnackbar } from '@graphcommerce/magento-cart'
2
+ import {
3
+ Button,
4
+ iconChevronRight,
5
+ IconSvg,
6
+ MessageSnackbar,
7
+ ErrorSnackbar,
8
+ } from '@graphcommerce/next-ui'
9
+ import { Trans } from '@lingui/react'
10
+ import PageLink from 'next/link'
11
+ import { useFormAddProductsToCart } from './AddProductsToCartForm'
12
+
13
+ type AddToCartMessageProps = { name?: string | null }
14
+
15
+ export function AddProductsToCartSnackbar(props: AddToCartMessageProps) {
16
+ const { name } = props
17
+ const { formState, error, data } = useFormAddProductsToCart()
18
+
19
+ return (
20
+ <>
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>
35
+
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' />
55
+ </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>
65
+ </>
66
+ )
67
+ }
@@ -0,0 +1,6 @@
1
+ export * from './AddProductsToCart.gql'
2
+ export * from './AddProductsToCartButton'
3
+ export * from './AddProductsToCartError'
4
+ export * from './AddProductsToCartForm'
5
+ export * from './AddProductsToCartQuantity'
6
+ export * from './AddProductsToCartSnackbar'
@@ -3,7 +3,7 @@ mutation ProductAddToCart(
3
3
  $sku: String!
4
4
  $quantity: Float = 1
5
5
  $enteredOptions: [EnteredOptionInput] = []
6
- $selectedOptions: [ID] = []
6
+ $selectedOptions: [ID!] = []
7
7
  ) {
8
8
  addProductsToCart(
9
9
  cartId: $cartId
@@ -0,0 +1,2 @@
1
+ export * from './ProductAddToCart'
2
+ export * from './ProductAddToCart.gql'
@@ -0,0 +1,93 @@
1
+ import { SelectElement, TextFieldElement } from '@graphcommerce/ecommerce-ui'
2
+ import { filterNonNullableKeys, RenderType, TypeRenderer } from '@graphcommerce/next-ui'
3
+ import { useFormAddProductsToCart } from '../AddProductsToCart'
4
+ import { ProductCustomizableFragment } from './ProductCustomizable.gql'
5
+
6
+ type OptionTypeRenderer = TypeRenderer<
7
+ NonNullable<NonNullable<ProductCustomizableFragment['options']>[number]> & {
8
+ optionIndex: number
9
+ index: number
10
+ }
11
+ >
12
+
13
+ const CustomizableAreaOption: OptionTypeRenderer['CustomizableAreaOption'] = (props) => {
14
+ const { uid, areaValue, required, optionIndex, index, title } = props
15
+ const maxLength = areaValue?.max_characters ?? undefined
16
+ const { control, register } = useFormAddProductsToCart()
17
+
18
+ return (
19
+ <>
20
+ <input
21
+ type='hidden'
22
+ {...register(`cartItems.${index}.entered_options.${optionIndex}.uid`)}
23
+ value={uid}
24
+ />
25
+ <TextFieldElement
26
+ multiline
27
+ minRows={3}
28
+ control={control}
29
+ name={`cartItems.${index}.entered_options.${optionIndex}.value`}
30
+ label={title}
31
+ required={Boolean(required)}
32
+ validation={{ maxLength }}
33
+ helperText={(maxLength ?? 0) > 0 && `A maximum of ${maxLength}`}
34
+ />
35
+ </>
36
+ )
37
+ }
38
+
39
+ const CustomizableDropDownOption: OptionTypeRenderer['CustomizableDropDownOption'] = (props) => {
40
+ const { uid, required, optionIndex, index, title, dropdownValue } = props
41
+ const { control, register } = useFormAddProductsToCart()
42
+
43
+ return (
44
+ <>
45
+ <input
46
+ type='hidden'
47
+ {...register(`cartItems.${index}.entered_options.${optionIndex}.uid`)}
48
+ value={uid}
49
+ />
50
+ <SelectElement
51
+ control={control}
52
+ name={`cartItems.${index}.entered_options.${optionIndex}.value`}
53
+ label={title}
54
+ required={Boolean(required)}
55
+ options={filterNonNullableKeys(dropdownValue, ['title']).map((option) => ({
56
+ id: option.uid,
57
+ label: option.title,
58
+ }))}
59
+ />
60
+ </>
61
+ )
62
+ }
63
+
64
+ const renderer: OptionTypeRenderer = {
65
+ CustomizableAreaOption,
66
+ CustomizableCheckboxOption: () => <div>checkbox not implemented</div>,
67
+ CustomizableDateOption: () => <div>date not implemented</div>,
68
+ CustomizableDropDownOption,
69
+ CustomizableFieldOption: () => <div>field not implemented</div>,
70
+ CustomizableFileOption: () => <div>file not implemented</div>,
71
+ CustomizableMultipleOption: () => <div>multi not implemented</div>,
72
+ CustomizableRadioOption: () => <div>radios not implemented</div>,
73
+ }
74
+
75
+ type ProductCustomizableProps = { product: ProductCustomizableFragment; index?: number }
76
+
77
+ export function ProductCustomizable(props: ProductCustomizableProps) {
78
+ const { product, index = 0 } = props
79
+
80
+ return (
81
+ <>
82
+ {filterNonNullableKeys(product.options, ['sort_order']).map((option) => (
83
+ <RenderType
84
+ key={option.uid}
85
+ renderer={renderer}
86
+ {...option}
87
+ optionIndex={option.sort_order - 1}
88
+ index={index}
89
+ />
90
+ ))}
91
+ </>
92
+ )
93
+ }
@@ -0,0 +1 @@
1
+ export * from './ProductCustomizable'
@@ -4,6 +4,7 @@ fragment ProductPageQueryFragment on Query {
4
4
  items {
5
5
  __typename
6
6
  uid
7
+ ...ProductWeight
7
8
  ...ProductPageItem
8
9
  }
9
10
  }
@@ -1,12 +1,12 @@
1
1
  import { SidebarGallery, SidebarGalleryProps, TypeRenderer } from '@graphcommerce/next-ui'
2
- import React, { PropsWithChildren } from 'react'
2
+ import { PropsWithChildren } from 'react'
3
3
  import { ProductPageGalleryFragment } from './ProductPageGallery.gql'
4
4
 
5
5
  export type ProductPageGalleryRenderers = TypeRenderer<
6
6
  NonNullable<NonNullable<ProductPageGalleryFragment['media_gallery']>[0]>
7
7
  >
8
8
 
9
- type ProductPageGalleryProps = PropsWithChildren<ProductPageGalleryFragment> &
9
+ export type ProductPageGalleryProps = PropsWithChildren<ProductPageGalleryFragment> &
10
10
  Omit<SidebarGalleryProps, 'sidebar' | 'images'>
11
11
 
12
12
  export function ProductPageGallery(props: ProductPageGalleryProps) {
@@ -0,0 +1 @@
1
+ export * from './ProductPagePrice.gql'
@@ -15,7 +15,13 @@ export function ProductShortDescription(props: ProductShortDescriptionProps) {
15
15
  component='div'
16
16
  className={classes.description}
17
17
  dangerouslySetInnerHTML={{ __html: short_description?.html ?? '' }}
18
- sx={[{ '& > p': { marginTop: 0 } }, ...(Array.isArray(sx) ? sx : [sx])]}
18
+ sx={[
19
+ {
20
+ '& > p:first-of-type': { marginTop: 0 },
21
+ '& > p:last-of-type': { marginBottom: 0 },
22
+ },
23
+ ...(Array.isArray(sx) ? sx : [sx]),
24
+ ]}
19
25
  />
20
26
  )
21
27
  }
@@ -22,7 +22,6 @@ export function ProductSidebarDelivery() {
22
22
  `,
23
23
  gridTemplateColumns: `min-content auto`,
24
24
  columnGap: theme.spacings.xxs,
25
- marginTop: theme.spacings.xxs,
26
25
  background:
27
26
  theme.palette.mode === 'light'
28
27
  ? darken(theme.palette.background.default, 0.01)
@@ -1,16 +1,16 @@
1
+ export * from './AddProductsToCart'
1
2
  export * from './JsonLdProduct/jsonLdProduct'
2
- export * from './ProductAddToCart/ProductAddToCart'
3
- export * from './ProductAddToCart/ProductAddToCart.gql'
3
+ export * from './ProductAddToCart'
4
4
  export * from './ProductList/ProductList.gql'
5
5
  export * from './ProductListCount/ProductListCount'
6
6
  export * from './ProductListFilters/ProductFilters.gql'
7
7
  export * from './ProductListFilters/ProductListFilters'
8
8
  export * from './ProductListFiltersContainer/ProductListFiltersContainer'
9
9
  export * from './ProductListItem/ProductListItem'
10
- export * from './ProductListItems/ProductListItems'
11
10
  export * from './ProductListItems/filteredProductList'
12
11
  export * from './ProductListItems/filterTypes'
13
12
  export * from './ProductListItems/getFilterTypes'
13
+ export * from './ProductListItems/ProductListItems'
14
14
  export * from './ProductListItems/ProductListItems.gql'
15
15
  export * from './ProductListItems/ProductListItemsBase'
16
16
  export * from './ProductListItems/ProductListParamsProvider'
@@ -22,6 +22,7 @@ export * from './ProductPageCategory/productPageCategory'
22
22
  export * from './ProductPageDescription/ProductPageDescription'
23
23
  export * from './ProductPageGallery/ProductPageGallery'
24
24
  export * from './ProductPageMeta/ProductPageMeta'
25
+ export * from './ProductPagePrice'
25
26
  export * from './ProductRelated/RelatedProducts.gql'
26
27
  export * from './ProductShortDescription/ProductShortDescription'
27
28
  export * from './ProductShortDescription/ProductShortDescription.gql'
@@ -30,3 +31,4 @@ export * from './ProductSpecs/ProductSpecs'
30
31
  export * from './ProductStaticPaths/getProductStaticPaths'
31
32
  export * from './ProductUpsells/UpsellProducts.gql'
32
33
  export * from './ProductWeight/ProductWeight'
34
+ export * from './ProductCustomizable'
@@ -3,16 +3,22 @@ import { ProductLinkFragment } from './ProductLink.gql'
3
3
  export type ProductLinkProps = Omit<ProductLinkFragment, 'uid'>
4
4
 
5
5
  export function productLink(link: ProductLinkProps) {
6
- const { __typename, url_key } = link
7
- const productRoute = __typename
8
- .split(/(?=[A-Z])/)
9
- .map((s) => s.toLowerCase())
10
- .reverse()
6
+ if (process.env.NEXT_PUBLIC_SINGLE_PRODUCT_PAGE !== '1') {
7
+ const { __typename, url_key } = link
11
8
 
12
- // For Simple and Virtual products we're not navigating to a type specific page
13
- if (__typename === 'SimpleProduct') productRoute.splice(1, 1)
9
+ const productRoute = __typename
10
+ .split(/(?=[A-Z])/)
11
+ .map((s) => s.toLowerCase())
12
+ .reverse()
14
13
 
15
- return `/${productRoute.join('/')}/${url_key}`
14
+ if (__typename === 'SimpleProduct')
15
+ // For Simple and Virtual products we're not navigating to a type specific page
16
+ productRoute.splice(1, 1)
17
+
18
+ return `/${productRoute.join('/')}/${url_key}`
19
+ }
20
+
21
+ return `/p/${link.url_key}`
16
22
  }
17
23
 
18
24
  export function useProductLink(props: ProductLinkProps) {
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.5.10",
5
+ "version": "4.6.0",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -19,13 +19,14 @@
19
19
  "type-fest": "^2.12.2"
20
20
  },
21
21
  "dependencies": {
22
- "@graphcommerce/framer-scroller": "2.1.38",
22
+ "@graphcommerce/ecommerce-ui": "1.5.0",
23
+ "@graphcommerce/framer-scroller": "2.1.39",
23
24
  "@graphcommerce/graphql": "3.4.8",
24
25
  "@graphcommerce/graphql-mesh": "4.2.0",
25
26
  "@graphcommerce/image": "3.1.9",
26
- "@graphcommerce/magento-cart": "4.8.3",
27
- "@graphcommerce/magento-store": "4.2.35",
28
- "@graphcommerce/next-ui": "4.26.0",
27
+ "@graphcommerce/magento-cart": "4.8.4",
28
+ "@graphcommerce/magento-store": "4.3.0",
29
+ "@graphcommerce/next-ui": "4.27.0",
29
30
  "schema-dts": "^1.1.0"
30
31
  },
31
32
  "peerDependencies": {