@graphcommerce/magento-cart-items 9.1.0-canary.18 → 9.1.0-canary.20

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.
@@ -14,10 +14,7 @@ fragment CartItem on CartItemInterface {
14
14
  }
15
15
  prices {
16
16
  discounts {
17
- amount {
18
- ...Money
19
- }
20
- label
17
+ ...Discount
21
18
  }
22
19
  price {
23
20
  ...Money
@@ -35,7 +32,6 @@ fragment CartItem on CartItemInterface {
35
32
  ...Money
36
33
  }
37
34
  }
38
-
39
35
  errors {
40
36
  code
41
37
  message
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.1.0-canary.20
4
+
5
+ ## 9.1.0-canary.19
6
+
7
+ ### Patch Changes
8
+
9
+ - [#2499](https://github.com/graphcommerce-org/graphcommerce/pull/2499) [`89e785d`](https://github.com/graphcommerce-org/graphcommerce/commit/89e785de9d62c2f6cf6b2885da72ff63b16fc70d) - Added support for TIME and DATE for the customizable options. Added required stars. ([@paales](https://github.com/paales))
10
+
11
+ - [#2499](https://github.com/graphcommerce-org/graphcommerce/pull/2499) [`6b2b44c`](https://github.com/graphcommerce-org/graphcommerce/commit/6b2b44ca853279144d7768067f3462d4d4bf0af1) - Created a new PriceModifiers component that is implemented on CartItems, allowing different product types to render their options in a consistent manner and allow rendering a base price so that the sum in the cart is correct. ([@paales](https://github.com/paales))
12
+
13
+ - [#2499](https://github.com/graphcommerce-org/graphcommerce/pull/2499) [`09945ab`](https://github.com/graphcommerce-org/graphcommerce/commit/09945abef32640d1000a1da3f376afe4776d697f) - Solve issue where the UpdateItemQuantity would send a wrong query to the backend. ([@paales](https://github.com/paales))
14
+
3
15
  ## 9.1.0-canary.18
4
16
 
5
17
  ## 9.1.0-canary.17
@@ -15,7 +15,7 @@ export function CartCrosssellsScroller(props: CartItemCrosssellsProps) {
15
15
  if (crossSellItems.length === 0 || crossSellsHideCartItems === true) return null
16
16
 
17
17
  return (
18
- <AddProductsToCartForm redirect={false} disableSuccessSnackbar>
18
+ <AddProductsToCartForm redirect={false} snackbarProps={{ disableSuccessSnackbar: true }}>
19
19
  <ProductScroller
20
20
  productListRenderer={renderer}
21
21
  items={crossSellItems}
@@ -1,7 +1,7 @@
1
1
  import { Image } from '@graphcommerce/image'
2
2
  import { useDisplayInclTax } from '@graphcommerce/magento-cart/hooks'
3
- import { productLink, productPath, type ProductLinkProps } from '@graphcommerce/magento-product'
4
- import { Money } from '@graphcommerce/magento-store'
3
+ import { productPath } from '@graphcommerce/magento-product'
4
+ import { Money, PriceModifiersList, type PriceModifier } from '@graphcommerce/magento-store'
5
5
  import type { ActionCardProps } from '@graphcommerce/next-ui'
6
6
  import { ActionCard, actionCardImageSizes, filterNonNullableKeys } from '@graphcommerce/next-ui'
7
7
  import { Trans } from '@lingui/react'
@@ -10,23 +10,22 @@ import type { CartItemFragment } from '../../Api/CartItem.gql'
10
10
  import { RemoveItemFromCart } from '../RemoveItemFromCart/RemoveItemFromCart'
11
11
  import { UpdateItemQuantity } from '../UpdateItemQuantity/UpdateItemQuantity'
12
12
 
13
- export type CartItemActionCardProps = { cartItem: CartItemFragment; readOnly?: boolean } & Omit<
14
- ActionCardProps,
15
- 'value' | 'image' | 'price' | 'title' | 'action'
16
- >
17
-
18
- /**
19
- * @deprecated
20
- * @public
21
- */
22
- export function productEditLink(link: ProductLinkProps) {
23
- return `/checkout/item/${link.url_key}`
24
- }
13
+ export type CartItemActionCardProps = {
14
+ cartItem: CartItemFragment
15
+ readOnly?: boolean
16
+ priceModifiers?: PriceModifier[]
17
+ } & Omit<ActionCardProps, 'value' | 'image' | 'title'>
25
18
 
26
19
  export function CartItemActionCard(props: CartItemActionCardProps) {
27
- const { cartItem, sx = [], size = 'responsive', readOnly = false, ...rest } = props
20
+ const {
21
+ cartItem,
22
+ sx = [],
23
+ size = 'responsive',
24
+ readOnly = false,
25
+ priceModifiers,
26
+ ...rest
27
+ } = props
28
28
  const { uid, quantity, prices, errors, product } = cartItem
29
- const { name, thumbnail, url_key } = product
30
29
 
31
30
  const inclTaxes = useDisplayInclTax()
32
31
 
@@ -89,10 +88,10 @@ export function CartItemActionCard(props: CartItemActionCardProps) {
89
88
  ...(Array.isArray(sx) ? sx : [sx]),
90
89
  ]}
91
90
  image={
92
- thumbnail?.url && (
91
+ product.thumbnail?.url ? (
93
92
  <Image
94
93
  layout='fill'
95
- src={thumbnail?.url}
94
+ src={product.thumbnail.url}
96
95
  sx={{
97
96
  width: actionCardImageSizes[size],
98
97
  height: actionCardImageSizes[size],
@@ -102,12 +101,14 @@ export function CartItemActionCard(props: CartItemActionCardProps) {
102
101
  }}
103
102
  sizes={actionCardImageSizes[size]}
104
103
  />
104
+ ) : (
105
+ <Box sx={{ width: actionCardImageSizes[size], height: actionCardImageSizes[size] }} />
105
106
  )
106
107
  }
107
108
  title={
108
- url_key ? (
109
+ product.url_key ? (
109
110
  <Link
110
- href={productPath(url_key)}
111
+ href={productPath(product.url_key)}
111
112
  underline='hover'
112
113
  sx={{
113
114
  color: 'inherit',
@@ -115,12 +116,20 @@ export function CartItemActionCard(props: CartItemActionCardProps) {
115
116
  maxWidth: 'max-content',
116
117
  }}
117
118
  >
118
- {name}
119
+ {product.name}
119
120
  </Link>
120
121
  ) : (
121
- name
122
+ product.name
122
123
  )
123
124
  }
125
+ size={size}
126
+ {...rest}
127
+ price={
128
+ <>
129
+ <Money {...(inclTaxes ? prices?.row_total_including_tax : prices?.row_total)} />
130
+ {rest.price}
131
+ </>
132
+ }
124
133
  secondaryAction={
125
134
  <>
126
135
  <Box
@@ -135,37 +144,51 @@ export function CartItemActionCard(props: CartItemActionCardProps) {
135
144
  >
136
145
  {readOnly ? quantity : <UpdateItemQuantity uid={uid} quantity={quantity} />}
137
146
  {' ⨉ '}
138
-
139
147
  <Money value={price} currency={prices?.price.currency} />
140
148
  </Box>
141
149
  {hasOptions && !readOnly && (
142
150
  <Button
143
151
  variant='inline'
144
152
  color='secondary'
145
- href={`${productEditLink(product)}?cartItemId=${uid}`}
153
+ href={`/checkout/item/${product.url_key}?cartItemId=${uid}`}
146
154
  >
147
155
  <Trans id='Edit options' />
148
156
  </Button>
149
157
  )}
158
+
159
+ {rest.secondaryAction}
160
+
161
+ {filterNonNullableKeys(errors).map((error) => (
162
+ <Box sx={{ color: 'error.main', typography: 'body2' }} key={error.message}>
163
+ {error.message}
164
+ </Box>
165
+ ))}
166
+ </>
167
+ }
168
+ details={
169
+ <>
170
+ {priceModifiers && priceModifiers.length > 0 && (
171
+ <PriceModifiersList
172
+ label={<Trans id='Base Price'>Base price</Trans>}
173
+ modifiers={[...priceModifiers]}
174
+ total={prices?.price_including_tax?.value ?? 0}
175
+ currency={prices?.price.currency}
176
+ />
177
+ )}
178
+ {rest.details}
150
179
  </>
151
180
  }
152
- price={<Money {...(inclTaxes ? prices?.row_total_including_tax : prices?.row_total)} />}
153
181
  action={
154
- !readOnly && (
155
- <RemoveItemFromCart
156
- {...cartItem}
157
- buttonProps={{ size: size === 'responsive' ? 'large' : size }}
158
- />
159
- )
182
+ <>
183
+ {!readOnly && (
184
+ <RemoveItemFromCart
185
+ {...cartItem}
186
+ buttonProps={{ size: size === 'responsive' ? 'large' : size }}
187
+ />
188
+ )}
189
+ {rest.action}
190
+ </>
160
191
  }
161
- size={size}
162
- after={filterNonNullableKeys(errors).map((error) => (
163
- <Box sx={{ color: 'error.main', typography: 'caption' }} key={error.message}>
164
- {error.message}
165
- </Box>
166
- ))}
167
- {...rest}
168
- details={<>{rest.details}</>}
169
192
  />
170
193
  )
171
194
  }
@@ -3,6 +3,4 @@ fragment EditCartItemForm on ProductInterface {
3
3
  ...ProductWeight
4
4
  ...ProductPageItem
5
5
  ...ConfigurableOptions
6
- ...DownloadableProductOptions
7
- ...BundleProductOptions
8
6
  }
@@ -1,4 +1,5 @@
1
1
  import { Money } from '@graphcommerce/magento-store'
2
+ import type { PriceModifier } from '@graphcommerce/magento-store/components/PriceModifiers'
2
3
  import { filterNonNullableKeys, nonNullable } from '@graphcommerce/next-ui'
3
4
  import { Box } from '@mui/material'
4
5
  import type { CartItemFragment } from '../../Api/CartItem.gql'
@@ -8,6 +9,26 @@ export type SelectedCustomizableOptionProps = CartItemFragment & {
8
9
  customizable_options?: (SelectedCustomizableOptionFragment | null | undefined)[] | null
9
10
  }
10
11
 
12
+ export function selectedCustomizableOptionsModifiers(
13
+ props: SelectedCustomizableOptionProps,
14
+ ): PriceModifier[] {
15
+ const { customizable_options, product } = props
16
+
17
+ return filterNonNullableKeys(customizable_options).map((option) => ({
18
+ key: option.customizable_option_uid,
19
+ label: option.label,
20
+ items: filterNonNullableKeys(option.values).map((value) => ({
21
+ key: value.customizable_option_value_uid,
22
+ label: value.label || value.value,
23
+ amount:
24
+ value.price.type === 'PERCENT'
25
+ ? (product.price_range.minimum_price.final_price.value ?? 0) * (value.price.value / 100)
26
+ : value.price.value,
27
+ })),
28
+ }))
29
+ }
30
+
31
+ /** @deprecated Replaced by `selectedCustomizableOptionsModifiers` */
11
32
  export function SelectedCustomizableOptions(props: SelectedCustomizableOptionProps) {
12
33
  const { customizable_options, product, prices } = props
13
34
  const options = filterNonNullableKeys(customizable_options, [])
@@ -38,7 +38,6 @@ export function UpdateItemQuantity(props: UpdateItemQuantityProps) {
38
38
 
39
39
  return (
40
40
  <form noValidate onSubmit={submit}>
41
- <FormAutoSubmit control={control} submit={submit} leading />
42
41
  <NumberFieldElement
43
42
  control={control}
44
43
  name='quantity'
@@ -51,7 +50,8 @@ export function UpdateItemQuantity(props: UpdateItemQuantityProps) {
51
50
  sx={sx}
52
51
  {...textInputProps}
53
52
  />
54
- <ApolloCartErrorSnackbar error={error} onClose={() => reset({ quantity })} />
53
+ <FormAutoSubmit control={control} submit={submit} leading trailing />
54
+ <ApolloCartErrorSnackbar error={error} onClose={() => reset({ quantity, uid })} />
55
55
  </form>
56
56
  )
57
57
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-cart-items",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "9.1.0-canary.18",
5
+ "version": "9.1.0-canary.20",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -12,19 +12,19 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@graphcommerce/ecommerce-ui": "^9.1.0-canary.18",
16
- "@graphcommerce/eslint-config-pwa": "^9.1.0-canary.18",
17
- "@graphcommerce/framer-next-pages": "^9.1.0-canary.18",
18
- "@graphcommerce/graphql": "^9.1.0-canary.18",
19
- "@graphcommerce/image": "^9.1.0-canary.18",
20
- "@graphcommerce/magento-cart": "^9.1.0-canary.18",
21
- "@graphcommerce/magento-customer": "^9.1.0-canary.18",
22
- "@graphcommerce/magento-product": "^9.1.0-canary.18",
23
- "@graphcommerce/magento-store": "^9.1.0-canary.18",
24
- "@graphcommerce/next-ui": "^9.1.0-canary.18",
25
- "@graphcommerce/prettier-config-pwa": "^9.1.0-canary.18",
26
- "@graphcommerce/react-hook-form": "^9.1.0-canary.18",
27
- "@graphcommerce/typescript-config-pwa": "^9.1.0-canary.18",
15
+ "@graphcommerce/ecommerce-ui": "^9.1.0-canary.20",
16
+ "@graphcommerce/eslint-config-pwa": "^9.1.0-canary.20",
17
+ "@graphcommerce/framer-next-pages": "^9.1.0-canary.20",
18
+ "@graphcommerce/graphql": "^9.1.0-canary.20",
19
+ "@graphcommerce/image": "^9.1.0-canary.20",
20
+ "@graphcommerce/magento-cart": "^9.1.0-canary.20",
21
+ "@graphcommerce/magento-customer": "^9.1.0-canary.20",
22
+ "@graphcommerce/magento-product": "^9.1.0-canary.20",
23
+ "@graphcommerce/magento-store": "^9.1.0-canary.20",
24
+ "@graphcommerce/next-ui": "^9.1.0-canary.20",
25
+ "@graphcommerce/prettier-config-pwa": "^9.1.0-canary.20",
26
+ "@graphcommerce/react-hook-form": "^9.1.0-canary.20",
27
+ "@graphcommerce/typescript-config-pwa": "^9.1.0-canary.20",
28
28
  "@lingui/core": "^4.2.1",
29
29
  "@lingui/macro": "^4.2.1",
30
30
  "@lingui/react": "^4.2.1",
@@ -1,16 +1,9 @@
1
- import type {
2
- AddProductsToCartFields,
3
- AnyOption,
4
- CustomizableProductOptionBase,
5
- OptionValueSelector,
6
- SelectorsProp,
7
- } from '@graphcommerce/magento-product'
8
- import { productCustomizableSelectors } from '@graphcommerce/magento-product'
1
+ import type { AddProductsToCartFields, SelectorsProp } from '@graphcommerce/magento-product'
9
2
  import { filterNonNullableKeys, isTypename, nonNullable } from '@graphcommerce/next-ui'
10
3
  import type { CartItemFragment } from '../Api/CartItem.gql'
11
4
  import type { EditCartItemFormFragment } from '../components/EditCartItem/EditCartItemForm/EditCartItemForm.gql'
12
5
 
13
- type CartItemInput = AddProductsToCartFields['cartItems'][number]
6
+ export type CartItemToCartItemInputReturnValue = AddProductsToCartFields['cartItems'][number]
14
7
 
15
8
  export type CartItemToCartItemInputProps = {
16
9
  product: EditCartItemFormFragment
@@ -19,19 +12,18 @@ export type CartItemToCartItemInputProps = {
19
12
 
20
13
  export function cartItemToCartItemInput(
21
14
  props: CartItemToCartItemInputProps,
22
- ): CartItemInput | undefined {
23
- const { product, cartItem, selectors } = props
15
+ ): CartItemToCartItemInputReturnValue | undefined {
16
+ const { product, cartItem } = props
24
17
 
25
18
  if (isTypename(product, ['GroupedProduct']) || !product.sku || !cartItem) return undefined
26
19
 
27
- const allSelectors: OptionValueSelector = { ...productCustomizableSelectors, ...selectors }
28
-
29
- const cartItemInput: CartItemInput = {
20
+ const cartItemInput = {
30
21
  sku: product.sku,
31
22
  quantity: cartItem.quantity,
32
- customizable_options: {},
23
+ selected_options_record: {},
33
24
  selected_options: [],
34
25
  entered_options: [],
26
+ entered_options_record: {},
35
27
  }
36
28
 
37
29
  const cartItemCustomizableOptions = filterNonNullableKeys(cartItem.customizable_options ?? {})
@@ -40,13 +32,6 @@ export function cartItemToCartItemInput(
40
32
  product.options?.filter(nonNullable).forEach((productOption) => {
41
33
  // @todo Date option: Magento's backend does not provide an ISO date string that can be used, only localized strings are available which can not be parsed.
42
34
  // @todo File option: We do not support file options yet.
43
- if (isTypename(productOption, ['CustomizableDateOption', 'CustomizableFileOption'])) return
44
-
45
- const selector = allSelectors[productOption.__typename] as
46
- | undefined
47
- | ((option: AnyOption) => CustomizableProductOptionBase | CustomizableProductOptionBase[])
48
- const possibleProductValues = selector ? selector(productOption) : null
49
-
50
35
  const cartItemCustomizableOption = cartItemCustomizableOptions.find(
51
36
  (option) => option?.customizable_option_uid === productOption.uid,
52
37
  )
@@ -56,55 +41,32 @@ export function cartItemToCartItemInput(
56
41
  )
57
42
  if (cartItemCustomizableOptionValue.length === 0) return
58
43
 
59
- if (Array.isArray(possibleProductValues)) {
60
- const value = cartItemCustomizableOptionValue.map((v) => v.customizable_option_value_uid)
61
- if (!cartItemInput.customizable_options) cartItemInput.customizable_options = {}
62
- cartItemInput.customizable_options[productOption.uid] = isTypename(productOption, [
63
- 'CustomizableRadioOption',
64
- 'CustomizableDropDownOption',
65
- ])
66
- ? value[0]
67
- : value
68
- } else {
69
- const idx = (productOption.sort_order ?? 0) + 100
70
-
71
- if (!cartItemInput.entered_options) cartItemInput.entered_options = []
72
- cartItemInput.entered_options[idx] = {
73
- uid: productOption.uid,
74
- value: cartItemCustomizableOptionValue[0].value,
75
- }
76
- }
77
- })
78
- }
79
-
80
- if (isTypename(cartItem, ['ConfigurableCartItem']) && cartItem.configurable_options) {
81
- cartItemInput.selected_options = filterNonNullableKeys(cartItem.configurable_options).map(
82
- (option) => option.configurable_product_option_value_uid,
83
- )
84
- }
85
-
86
- if (isTypename(cartItem, ['BundleCartItem']) && isTypename(product, ['BundleProduct'])) {
87
- filterNonNullableKeys(product.items).forEach((productBundleItem) => {
88
- const cartItemBundleOption = cartItem.bundle_options.find(
89
- (option) => option?.uid === productBundleItem?.uid,
90
- )
91
-
92
- if (!cartItemBundleOption) return
93
-
94
- // todo multi select..
95
- const idx = productBundleItem.position ?? 0 + 1000
96
- const value = cartItemBundleOption.values[0]
97
-
98
- if (!value) return
99
- if (productBundleItem.options?.some((o) => o?.can_change_quantity)) {
100
- if (!cartItemInput.entered_options) cartItemInput.entered_options = []
101
- cartItemInput.entered_options[idx] = {
102
- uid: value.uid,
103
- value: `${value.quantity}`,
104
- }
105
- } else {
106
- if (!cartItemInput.selected_options) cartItemInput.selected_options = []
107
- cartItemInput.selected_options[idx] = value.uid
44
+ switch (productOption.__typename) {
45
+ case 'CustomizableAreaOption':
46
+ case 'CustomizableFileOption':
47
+ case 'CustomizableFieldOption':
48
+ cartItemCustomizableOptionValue.forEach(
49
+ ({ customizable_option_value_uid: uid, value }) => {
50
+ cartItemInput.entered_options_record[uid] = value
51
+ },
52
+ )
53
+ break
54
+ case 'CustomizableRadioOption':
55
+ case 'CustomizableDropDownOption':
56
+ cartItemInput.selected_options_record[productOption.uid] =
57
+ cartItemCustomizableOptionValue[0]?.customizable_option_value_uid
58
+ break
59
+ case 'CustomizableCheckboxOption':
60
+ case 'CustomizableMultipleOption':
61
+ cartItemInput.selected_options_record[productOption.uid] =
62
+ cartItemCustomizableOptionValue.map(({ customizable_option_value_uid: uid }) => uid)
63
+ break
64
+ case 'CustomizableDateOption':
65
+ // Not supported, backend does not provide an ISO date string that can be used, only localized strings are available which can not be parsed.
66
+ break
67
+ default:
68
+ console.log(`${(productOption as { __typename: string }).__typename} not implemented`)
69
+ break
108
70
  }
109
71
  })
110
72
  }