@graphcommerce/magento-product-bundle 9.1.0-canary.17 → 9.1.0-canary.19

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/components/BundleProductOptions/BundleOption.tsx +22 -16
  3. package/components/BundleProductOptions/BundleOptionValue.tsx +25 -36
  4. package/components/BundleProductOptions/BundleProductOptions.tsx +8 -8
  5. package/components/BundleProductOptions/calculateBundleOptionValuePrice.ts +45 -0
  6. package/components/BundleProductOptions/types.ts +24 -10
  7. package/{ProductListItemBundle.tsx → components/ProductListItemBundle/ProductListItemBundle.tsx} +1 -1
  8. package/components/index.ts +6 -0
  9. package/{components/BundleCartItem → graphql/fragments}/BundleCartItem.graphql +1 -9
  10. package/graphql/fragments/ItemSelectedBundleOption.graphql +13 -0
  11. package/graphql/fragments/SelectedBundleOption.graphql +11 -0
  12. package/graphql/index.ts +9 -0
  13. package/graphql/inject/CreditMemoItem_Bundle.graphql +5 -0
  14. package/graphql/inject/InvoiceItem_Bundle.graphql +5 -0
  15. package/graphql/inject/OrderItem_Bundle.graphql +5 -0
  16. package/{components/BundleProductOptions/BundleProductOptions.graphql → graphql/inject/ProductPageItem_Bundle.graphql} +20 -1
  17. package/graphql/inject/ShipmentItem_Bundle.graphql +5 -0
  18. package/index.ts +2 -3
  19. package/package.json +14 -14
  20. package/plugins/BundleCartItemActionCard.tsx +25 -10
  21. package/plugins/BundleCreditMemoItem.tsx +33 -0
  22. package/plugins/BundleInvoiceItem.tsx +33 -0
  23. package/plugins/BundleOrderItem.tsx +33 -0
  24. package/plugins/BundleProductPagePrice.tsx +117 -0
  25. package/plugins/BundleShipmentItem.tsx +33 -0
  26. package/plugins/Bundle_cartItemToCartItemInput.ts +48 -0
  27. package/BundleProductPage.graphql +0 -3
  28. package/ProductPageBundleQueryFragment.graphql +0 -9
  29. package/components/BundleCartItem/BundleCartItem.tsx +0 -36
  30. package/components/BundleProductCartItemOptions/BundleProductCartItemOptions.tsx +0 -42
  31. /package/{ProductListItemBundle.graphql → graphql/inject/ProductListItemBundle.graphql} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.1.0-canary.19
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2499](https://github.com/graphcommerce-org/graphcommerce/pull/2499) [`224945f`](https://github.com/graphcommerce-org/graphcommerce/commit/224945faf04dff48692b7fcd99e1835d8a683143) - Add the ability to edit bundle products from the cart page ([@paales](https://github.com/paales))
8
+
9
+ - [#2499](https://github.com/graphcommerce-org/graphcommerce/pull/2499) [`224945f`](https://github.com/graphcommerce-org/graphcommerce/commit/224945faf04dff48692b7fcd99e1835d8a683143) - Magento 2.4.7: Render discounts for bundle products ([@paales](https://github.com/paales))
10
+
11
+ - [#2499](https://github.com/graphcommerce-org/graphcommerce/pull/2499) [`224945f`](https://github.com/graphcommerce-org/graphcommerce/commit/224945faf04dff48692b7fcd99e1835d8a683143) - Calculate the product page price dynamically based on the options and quantities selected. ([@paales](https://github.com/paales))
12
+
13
+ - [#2499](https://github.com/graphcommerce-org/graphcommerce/pull/2499) [`6b2b44c`](https://github.com/graphcommerce-org/graphcommerce/commit/6b2b44ca853279144d7768067f3462d4d4bf0af1) - Implement the Cart options as priceModifiers so the logic can be somewhat re-used for multiple locations ([@paales](https://github.com/paales))
14
+
15
+ - [#2499](https://github.com/graphcommerce-org/graphcommerce/pull/2499) [`2409514`](https://github.com/graphcommerce-org/graphcommerce/commit/240951428ac0bdc11649f4190b6d51c004680b34) - Order/Invoice/CreditMemo and Shipment views ([@paales](https://github.com/paales))
16
+
17
+ ## 9.1.0-canary.18
18
+
3
19
  ## 9.1.0-canary.17
4
20
 
5
21
  ## 9.1.0-canary.16
@@ -6,20 +6,27 @@ import { filterNonNullableKeys, SectionHeader } from '@graphcommerce/next-ui'
6
6
  import { i18n } from '@lingui/core'
7
7
  import React, { useMemo } from 'react'
8
8
  import { BundleOptionValue } from './BundleOptionValue'
9
- import type { BundleOptionProps, BundleOptionValueProps } from './types'
9
+ import { toBundleOptionType, type BundleOptionProps, type BundleOptionValueProps } from './types'
10
10
 
11
11
  export const BundleOption = React.memo<BundleOptionProps>((props) => {
12
- const { idx, index, options, title, color, layout, size, variant, required: _required } = props
12
+ const { index, item, color, layout, size, variant, product, renderer } = props
13
+ const { options, title, required, type: incomingType, uid, price_range } = item
13
14
  const { control } = useFormAddProductsToCart()
14
-
15
- const required = _required ?? false
15
+ const type = toBundleOptionType(incomingType)
16
16
 
17
17
  return (
18
18
  <div>
19
- <SectionHeader labelLeft={title} sx={{ mt: 0 }} />
19
+ <SectionHeader
20
+ labelLeft={
21
+ <>
22
+ {title} {required && ' *'}
23
+ </>
24
+ }
25
+ />
20
26
  <ActionCardListForm<BundleOptionValueProps & ActionCardItemBase, AddProductsToCartFields>
21
27
  control={control}
22
- required={required}
28
+ required={Boolean(required)}
29
+ multiple={type === 'checkbox' || type === 'multi'}
23
30
  color={color}
24
31
  layout={layout}
25
32
  size={size}
@@ -30,22 +37,21 @@ export const BundleOption = React.memo<BundleOptionProps>((props) => {
30
37
  ? i18n._(/* i18n*/ 'Please select a value for ‘{label}’', { label: title })
31
38
  : false,
32
39
  }}
33
- name={
34
- options?.some((o) => o?.can_change_quantity)
35
- ? `cartItems.${index}.entered_options.${idx}.uid`
36
- : `cartItems.${index}.selected_options.${idx}`
37
- }
38
- render={BundleOptionValue}
40
+ name={`cartItems.${index}.selected_options_record.${uid}`}
41
+ render={renderer ?? BundleOptionValue}
42
+ requireOptionSelection={Boolean(required)}
39
43
  items={useMemo(
40
44
  () =>
41
45
  filterNonNullableKeys(options).map((option) => ({
42
- ...option,
46
+ product,
47
+ item,
48
+ option,
43
49
  value: option.uid,
44
- idx,
45
50
  index,
46
- required,
51
+ dynamicPrice: product.dynamic_price ?? false,
52
+ discountPercent: price_range.minimum_price.discount?.percent_off ?? 0,
47
53
  })),
48
- [idx, index, options, required],
54
+ [index, options, required],
49
55
  )}
50
56
  />
51
57
  </div>
@@ -1,10 +1,12 @@
1
1
  import type { ActionCardItemRenderProps } from '@graphcommerce/ecommerce-ui'
2
2
  import { NumberFieldElement } from '@graphcommerce/ecommerce-ui'
3
3
  import { Image } from '@graphcommerce/image'
4
- import { useFormAddProductsToCart } from '@graphcommerce/magento-product'
5
- import { Money } from '@graphcommerce/magento-store'
6
- import { ActionCard, Button, responsiveVal } from '@graphcommerce/next-ui'
7
- import { Trans } from '@lingui/react'
4
+ import { ProductListPrice, useFormAddProductsToCart } from '@graphcommerce/magento-product'
5
+ import { ActionCard, responsiveVal } from '@graphcommerce/next-ui'
6
+ import {
7
+ calculateBundleOptionValuePrice,
8
+ toProductListPriceFragment,
9
+ } from './calculateBundleOptionValuePrice'
8
10
  import type { BundleOptionValueProps } from './types'
9
11
 
10
12
  const swatchSizes = {
@@ -16,27 +18,30 @@ const swatchSizes = {
16
18
  export function BundleOptionValue(props: ActionCardItemRenderProps<BundleOptionValueProps>) {
17
19
  const {
18
20
  selected,
19
- idx,
20
- index,
21
- price,
21
+ item,
22
+ option,
22
23
  product,
23
- label,
24
+ index,
24
25
  size = 'large',
25
26
  color,
26
- can_change_quantity,
27
- quantity = 1,
28
- required,
27
+ price,
29
28
  onReset,
29
+ ...rest
30
30
  } = props
31
31
  const { control } = useFormAddProductsToCart()
32
32
 
33
- const thumbnail = product?.thumbnail?.url
33
+ const thumbnail = option.product?.thumbnail?.url
34
+
35
+ const pricing = toProductListPriceFragment(
36
+ calculateBundleOptionValuePrice(product, item, option),
37
+ item.price_range.minimum_price.final_price.currency,
38
+ )
34
39
 
35
40
  return (
36
41
  <ActionCard
37
42
  {...props}
38
- title={label}
39
- price={price ? <Money value={price} /> : undefined}
43
+ title={option.label}
44
+ price={<ProductListPrice {...pricing} />}
40
45
  image={
41
46
  thumbnail &&
42
47
  !thumbnail.includes('/placeholder/') && (
@@ -44,7 +49,7 @@ export function BundleOptionValue(props: ActionCardItemRenderProps<BundleOptionV
44
49
  src={thumbnail}
45
50
  width={40}
46
51
  height={40}
47
- alt={label ?? ''}
52
+ alt={option.label ?? option.product?.name ?? ''}
48
53
  sizes={swatchSizes[size]}
49
54
  sx={{
50
55
  display: 'block',
@@ -55,40 +60,24 @@ export function BundleOptionValue(props: ActionCardItemRenderProps<BundleOptionV
55
60
  />
56
61
  )
57
62
  }
58
- action={
59
- (can_change_quantity || !required) && (
60
- <Button disableRipple variant='inline' color='inherit' size='small' tabIndex={-1}>
61
- <Trans id='Select' />
62
- </Button>
63
- )
64
- }
65
- reset={
66
- (can_change_quantity || !required) && (
67
- <Button disableRipple variant='inline' color='inherit' size='small' onClick={onReset}>
68
- {can_change_quantity ? <Trans id='Change' /> : <Trans id='Remove' />}
69
- </Button>
70
- )
71
- }
63
+ reset={<></>}
72
64
  secondaryAction={
73
65
  selected &&
74
- can_change_quantity && (
66
+ option.can_change_quantity && (
75
67
  <NumberFieldElement
76
68
  size='small'
77
69
  label='Quantity'
78
70
  color={color}
79
71
  inputProps={{ min: 1 }}
80
72
  required
81
- defaultValue={`${quantity}`}
73
+ defaultValue={`${option.quantity}`}
82
74
  control={control}
83
75
  sx={{
84
76
  width: responsiveVal(80, 120),
85
77
  mt: 2,
86
- '& .MuiFormHelperText-root': {
87
- margin: 1,
88
- width: '100%',
89
- },
78
+ '& .MuiFormHelperText-root': { margin: 1, width: '100%' },
90
79
  }}
91
- name={`cartItems.${index}.entered_options.${idx}.value`}
80
+ name={`cartItems.${index}.entered_options_record.${option.uid}`}
92
81
  onMouseDown={(e) => e.stopPropagation()}
93
82
  />
94
83
  )
@@ -1,9 +1,9 @@
1
1
  import type { AddToCartItemSelector } from '@graphcommerce/magento-product'
2
2
  import type { ActionCardListProps } from '@graphcommerce/next-ui'
3
- import { filterNonNullableKeys } from '@graphcommerce/next-ui'
3
+ import { nonNullable } from '@graphcommerce/next-ui'
4
+ import type { ProductPageItem_BundleFragment } from '../../graphql'
4
5
  import { BundleOption } from './BundleOption'
5
6
  import { BundleOptionValue } from './BundleOptionValue'
6
- import type { BundleProductOptionsFragment } from './BundleProductOptions.gql'
7
7
  import type { BundleOptionValueProps } from './types'
8
8
 
9
9
  export type BundelProductOptionsProps = Pick<
@@ -11,22 +11,22 @@ export type BundelProductOptionsProps = Pick<
11
11
  'size' | 'layout' | 'color' | 'variant'
12
12
  > & {
13
13
  renderer?: React.FC<BundleOptionValueProps>
14
- product: BundleProductOptionsFragment
14
+ product: ProductPageItem_BundleFragment
15
15
  } & AddToCartItemSelector
16
16
 
17
17
  export function BundleProductOptions(props: BundelProductOptionsProps) {
18
- const { product, index = 0 } = props
18
+ const { product, index = 0, ...rest } = props
19
19
 
20
20
  return (
21
21
  <>
22
- {filterNonNullableKeys(product?.items, ['uid', 'title', 'type']).map((item) => (
22
+ {(product?.items ?? []).filter(nonNullable).map((item) => (
23
23
  <BundleOption
24
24
  index={index}
25
25
  key={item.uid}
26
26
  color='primary'
27
- {...props}
28
- {...item}
29
- idx={item.position ?? 0 + 1000}
27
+ item={item}
28
+ product={product}
29
+ {...rest}
30
30
  renderer={BundleOptionValue}
31
31
  />
32
32
  ))}
@@ -0,0 +1,45 @@
1
+ import type { CurrencyEnum } from '@graphcommerce/graphql-mesh'
2
+ import type { ProductListPriceFragment } from '@graphcommerce/magento-product/components'
3
+ import type { BundleProductItemOptionType, BundleProductItemType, BundleProductType } from './types'
4
+
5
+ export type CalculatedBundleOptionValuePrice = [number, number] // [regularPrice, finalPrice]
6
+
7
+ export function calculateBundleOptionValuePrice(
8
+ product: BundleProductType,
9
+ item: BundleProductItemType,
10
+ option: BundleProductItemOptionType,
11
+ quantity = 1,
12
+ ): CalculatedBundleOptionValuePrice {
13
+ const { dynamic_price = false } = product
14
+ const precentOff = item?.price_range.minimum_price.discount?.percent_off ?? 0
15
+
16
+ const regularPrice =
17
+ (dynamic_price ? option.product?.price_range.minimum_price.final_price.value : option.price) ??
18
+ 0
19
+
20
+ const finalPrice = regularPrice * (1 - precentOff / 100)
21
+ return [regularPrice * quantity, finalPrice * quantity]
22
+ }
23
+
24
+ export function sumCalculatedBundleOptionValuePrices(
25
+ prices: CalculatedBundleOptionValuePrice[],
26
+ ): CalculatedBundleOptionValuePrice {
27
+ return prices.reduce((acc, [regular, final]) => [acc[0] + regular, acc[1] + final], [0, 0])
28
+ }
29
+
30
+ export function substractCalculatedBundleOptionValuePrices(
31
+ basePrice: CalculatedBundleOptionValuePrice,
32
+ substractPrice: CalculatedBundleOptionValuePrice,
33
+ ): CalculatedBundleOptionValuePrice {
34
+ return [basePrice[0] - substractPrice[0], basePrice[1] - substractPrice[1]]
35
+ }
36
+
37
+ export function toProductListPriceFragment(
38
+ price: CalculatedBundleOptionValuePrice,
39
+ currency: CurrencyEnum | null | undefined,
40
+ ): ProductListPriceFragment {
41
+ return {
42
+ regular_price: { currency: currency, value: price[0] },
43
+ final_price: { currency, value: price[1] },
44
+ }
45
+ }
@@ -1,18 +1,32 @@
1
1
  import type { ActionCardItemRenderProps } from '@graphcommerce/ecommerce-ui'
2
2
  import type { ActionCardListProps } from '@graphcommerce/next-ui'
3
- import type { BundleProductOptionsFragment } from './BundleProductOptions.gql'
3
+ import type { ProductPageItem_BundleFragment } from '../../graphql'
4
+
5
+ export type BundleProductType = ProductPageItem_BundleFragment
6
+ export type BundleProductItemType = NonNullable<NonNullable<BundleProductType['items']>[number]>
7
+ export type BundleProductItemOptionType = NonNullable<
8
+ NonNullable<BundleProductItemType['options']>[number]
9
+ >
4
10
 
5
11
  export type BundleOptionProps = {
6
- idx: number
7
- index: number
8
12
  renderer?: React.FC<ActionCardItemRenderProps<BundleOptionValueProps>>
9
- } & NonNullable<NonNullable<BundleProductOptionsFragment['items']>[number]> &
10
- Pick<ActionCardListProps, 'size' | 'layout' | 'color' | 'variant'>
13
+ index: number
14
+ product: BundleProductType
15
+ item: BundleProductItemType
16
+ } & Pick<ActionCardListProps, 'size' | 'layout' | 'color' | 'variant'>
11
17
 
12
- export type BundleOptionValueProps = NonNullable<
13
- NonNullable<BundleOptionProps['options']>[number]
14
- > & {
15
- idx: number
18
+ export type BundleOptionValueProps = {
16
19
  index: number
17
- required: boolean
20
+ product: BundleProductType
21
+ item: BundleProductItemType
22
+ option: BundleProductItemOptionType
23
+ }
24
+
25
+ const possibleTypes = ['radio', 'checkbox', 'multi', 'select'] as const
26
+ export type BundleOptionType = (typeof possibleTypes)[number]
27
+
28
+ export function toBundleOptionType(type: string | null | undefined): BundleOptionType {
29
+ if (!type) return 'radio'
30
+ if (possibleTypes.includes(type as BundleOptionType)) return type as BundleOptionType
31
+ return 'radio'
18
32
  }
@@ -1,6 +1,6 @@
1
1
  import type { ProductListItemProps } from '@graphcommerce/magento-product'
2
2
  import { ProductListItem } from '@graphcommerce/magento-product'
3
- import type { ProductListItemBundleFragment } from './ProductListItemBundle.gql'
3
+ import type { ProductListItemBundleFragment } from '../../graphql'
4
4
 
5
5
  export type ProdustListItemBundleProps = ProductListItemBundleFragment & ProductListItemProps
6
6
 
@@ -0,0 +1,6 @@
1
+ export * from './ProductListItemBundle/ProductListItemBundle'
2
+ export * from './BundleProductOptions/BundleOption'
3
+ export * from './BundleProductOptions/BundleOptionValue'
4
+ export * from './BundleProductOptions/BundleProductOptions'
5
+ export * from './BundleProductOptions/calculateBundleOptionValuePrice'
6
+ export * from './BundleProductOptions/types'
@@ -5,15 +5,7 @@ fragment BundleCartItem on BundleCartItem @inject(into: ["CartItem"]) {
5
5
  }
6
6
  }
7
7
  bundle_options {
8
- uid
9
- label
10
- type
11
- values {
12
- uid
13
- label
14
- quantity
15
- price
16
- }
8
+ ...SelectedBundleOption
17
9
  }
18
10
  customizable_options {
19
11
  ...SelectedCustomizableOption
@@ -0,0 +1,13 @@
1
+ fragment ItemSelectedBundleOption on ItemSelectedBundleOption {
2
+ label
3
+ uid
4
+ values {
5
+ price {
6
+ ...Money
7
+ }
8
+ product_name
9
+ product_sku
10
+ quantity
11
+ uid
12
+ }
13
+ }
@@ -0,0 +1,11 @@
1
+ fragment SelectedBundleOption on SelectedBundleOption {
2
+ uid
3
+ label
4
+ type
5
+ values {
6
+ uid
7
+ label
8
+ quantity
9
+ price
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ export * from './fragments/BundleCartItem.gql'
2
+ export * from './fragments/ItemSelectedBundleOption.gql'
3
+ export * from './fragments/SelectedBundleOption.gql'
4
+ export * from './inject/CreditMemoItem_Bundle.gql'
5
+ export * from './inject/InvoiceItem_Bundle.gql'
6
+ export * from './inject/OrderItem_Bundle.gql'
7
+ export * from './inject/ProductListItemBundle.gql'
8
+ export * from './inject/ProductPageItem_Bundle.gql'
9
+ export * from './inject/ShipmentItem_Bundle.gql'
@@ -0,0 +1,5 @@
1
+ fragment CreditMemoItem_Bundle on BundleCreditMemoItem @inject(into: ["CreditMemoItem"]) {
2
+ bundle_options {
3
+ ...ItemSelectedBundleOption
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ fragment InvoiceItem_Bundle on BundleInvoiceItem @inject(into: ["InvoiceItem"]) {
2
+ bundle_options {
3
+ ...ItemSelectedBundleOption
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ fragment OrderItem_Bundle on BundleOrderItem @inject(into: ["OrderItem"]) {
2
+ bundle_options {
3
+ ...ItemSelectedBundleOption
4
+ }
5
+ }
@@ -1,9 +1,20 @@
1
- fragment BundleProductOptions on BundleProduct {
1
+ fragment ProductPageItem_Bundle on BundleProduct @inject(into: ["ProductPageItem"]) {
2
2
  ship_bundle_items
3
3
  dynamic_sku
4
4
  dynamic_price
5
5
  dynamic_weight
6
+ price_view
6
7
  items {
8
+ price_range {
9
+ minimum_price {
10
+ final_price {
11
+ currency
12
+ }
13
+ discount {
14
+ percent_off
15
+ }
16
+ }
17
+ }
7
18
  uid
8
19
  position
9
20
  required
@@ -11,6 +22,7 @@ fragment BundleProductOptions on BundleProduct {
11
22
  title
12
23
  type
13
24
  options {
25
+ __typename
14
26
  can_change_quantity
15
27
  uid
16
28
  is_default
@@ -29,6 +41,13 @@ fragment BundleProductOptions on BundleProduct {
29
41
  thumbnail {
30
42
  ...ProductImage
31
43
  }
44
+ price_range {
45
+ minimum_price {
46
+ final_price {
47
+ ...Money
48
+ }
49
+ }
50
+ }
32
51
  }
33
52
  }
34
53
  }
@@ -0,0 +1,5 @@
1
+ fragment ShipmentItem_Bundle on BundleShipmentItem @inject(into: ["ShipmentItem"]) {
2
+ bundle_options {
3
+ ...ItemSelectedBundleOption
4
+ }
5
+ }
package/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from './ProductListItemBundle'
2
- export * from './components/BundleCartItem/BundleCartItem'
3
- export * from './BundleProductPage.gql'
1
+ export * from './components/ProductListItemBundle/ProductListItemBundle'
4
2
  export * from './components/BundleProductOptions'
3
+ export * from './graphql'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-product-bundle",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "9.1.0-canary.17",
5
+ "version": "9.1.0-canary.19",
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.17",
16
- "@graphcommerce/eslint-config-pwa": "^9.1.0-canary.17",
17
- "@graphcommerce/graphql": "^9.1.0-canary.17",
18
- "@graphcommerce/image": "^9.1.0-canary.17",
19
- "@graphcommerce/magento-cart": "^9.1.0-canary.17",
20
- "@graphcommerce/magento-cart-items": "^9.1.0-canary.17",
21
- "@graphcommerce/magento-product": "^9.1.0-canary.17",
22
- "@graphcommerce/magento-product-simple": "^9.1.0-canary.17",
23
- "@graphcommerce/magento-product-virtual": "^9.1.0-canary.17",
24
- "@graphcommerce/magento-store": "^9.1.0-canary.17",
25
- "@graphcommerce/next-ui": "^9.1.0-canary.17",
26
- "@graphcommerce/prettier-config-pwa": "^9.1.0-canary.17",
27
- "@graphcommerce/typescript-config-pwa": "^9.1.0-canary.17",
15
+ "@graphcommerce/ecommerce-ui": "^9.1.0-canary.19",
16
+ "@graphcommerce/eslint-config-pwa": "^9.1.0-canary.19",
17
+ "@graphcommerce/graphql": "^9.1.0-canary.19",
18
+ "@graphcommerce/image": "^9.1.0-canary.19",
19
+ "@graphcommerce/magento-cart": "^9.1.0-canary.19",
20
+ "@graphcommerce/magento-cart-items": "^9.1.0-canary.19",
21
+ "@graphcommerce/magento-product": "^9.1.0-canary.19",
22
+ "@graphcommerce/magento-product-simple": "^9.1.0-canary.19",
23
+ "@graphcommerce/magento-product-virtual": "^9.1.0-canary.19",
24
+ "@graphcommerce/magento-store": "^9.1.0-canary.19",
25
+ "@graphcommerce/next-ui": "^9.1.0-canary.19",
26
+ "@graphcommerce/prettier-config-pwa": "^9.1.0-canary.19",
27
+ "@graphcommerce/typescript-config-pwa": "^9.1.0-canary.19",
28
28
  "@lingui/core": "^4.2.1",
29
29
  "@lingui/macro": "^4.2.1",
30
30
  "@lingui/react": "^4.2.1",
@@ -1,7 +1,10 @@
1
- import type { CartItemActionCardProps } from '@graphcommerce/magento-cart-items'
1
+ import {
2
+ selectedCustomizableOptionsModifiers,
3
+ type CartItemActionCardProps,
4
+ } from '@graphcommerce/magento-cart-items'
5
+ import type { PriceModifier } from '@graphcommerce/magento-store'
2
6
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
- import { isTypename } from '@graphcommerce/next-ui'
4
- import { BundleProductCartItemOptions } from '../components/BundleProductCartItemOptions/BundleProductCartItemOptions'
7
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
5
8
 
6
9
  export const config: PluginConfig = {
7
10
  type: 'component',
@@ -11,17 +14,29 @@ export const config: PluginConfig = {
11
14
  export function CartItemActionCard(props: PluginProps<CartItemActionCardProps>) {
12
15
  const { Prev, ...rest } = props
13
16
 
14
- if (!isTypename(rest.cartItem, ['BundleCartItem'])) return <Prev {...rest} />
17
+ if (rest.cartItem.__typename !== 'BundleCartItem') return <Prev {...rest} />
18
+
19
+ const bundleModifiers = filterNonNullableKeys(rest.cartItem.bundle_options).map<PriceModifier>(
20
+ (option) => ({
21
+ key: option.uid,
22
+ label: option.label,
23
+ items: filterNonNullableKeys(option.values).map((value) => ({
24
+ key: value.uid,
25
+ label: value.label,
26
+ amount: value.price,
27
+ quantity: value.quantity,
28
+ })),
29
+ }),
30
+ )
15
31
 
16
32
  return (
17
33
  <Prev
18
34
  {...rest}
19
- details={
20
- <>
21
- {rest.details}
22
- <BundleProductCartItemOptions {...rest.cartItem} />
23
- </>
24
- }
35
+ priceModifiers={[
36
+ ...(rest.priceModifiers ?? []),
37
+ ...bundleModifiers,
38
+ ...selectedCustomizableOptionsModifiers(rest.cartItem),
39
+ ]}
25
40
  />
26
41
  )
27
42
  }
@@ -0,0 +1,33 @@
1
+ import type { CreditMemoItemProps } from '@graphcommerce/magento-customer'
2
+ import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
4
+
5
+ export const config: PluginConfig = {
6
+ type: 'component',
7
+ module: '@graphcommerce/magento-customer',
8
+ }
9
+
10
+ export function CreditMemoItem(props: PluginProps<CreditMemoItemProps>) {
11
+ const { Prev, ...rest } = props
12
+
13
+ if (rest.item.__typename !== 'BundleCreditMemoItem') return <Prev {...rest} />
14
+
15
+ return (
16
+ <Prev
17
+ {...rest}
18
+ priceModifiers={[
19
+ ...(rest.priceModifiers ?? []),
20
+ ...filterNonNullableKeys(rest.item.bundle_options).map((option) => ({
21
+ key: option.uid,
22
+ label: option.label,
23
+ items: filterNonNullableKeys(option.values).map((value, index) => ({
24
+ key: `${index}`,
25
+ label: value.product_name,
26
+ amount: value.price.value ?? 0,
27
+ quantity: value.quantity,
28
+ })),
29
+ })),
30
+ ]}
31
+ />
32
+ )
33
+ }
@@ -0,0 +1,33 @@
1
+ import type { InvoiceItemProps } from '@graphcommerce/magento-customer'
2
+ import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
4
+
5
+ export const config: PluginConfig = {
6
+ type: 'component',
7
+ module: '@graphcommerce/magento-customer',
8
+ }
9
+
10
+ export function InvoiceItem(props: PluginProps<InvoiceItemProps>) {
11
+ const { Prev, ...rest } = props
12
+
13
+ if (rest.item.__typename !== 'BundleInvoiceItem') return <Prev {...rest} />
14
+
15
+ return (
16
+ <Prev
17
+ {...rest}
18
+ priceModifiers={[
19
+ ...(rest.priceModifiers ?? []),
20
+ ...filterNonNullableKeys(rest.item.bundle_options).map((option) => ({
21
+ key: option.uid,
22
+ label: option.label,
23
+ items: filterNonNullableKeys(option.values).map((value, index) => ({
24
+ key: `${index}`,
25
+ label: value.product_name,
26
+ amount: value.price.value ?? 0,
27
+ quantity: value.quantity,
28
+ })),
29
+ })),
30
+ ]}
31
+ />
32
+ )
33
+ }
@@ -0,0 +1,33 @@
1
+ import type { OrderItemProps } from '@graphcommerce/magento-customer'
2
+ import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
4
+
5
+ export const config: PluginConfig = {
6
+ type: 'component',
7
+ module: '@graphcommerce/magento-customer',
8
+ }
9
+
10
+ export function OrderItem(props: PluginProps<OrderItemProps>) {
11
+ const { Prev, ...rest } = props
12
+
13
+ if (rest.item.__typename !== 'BundleOrderItem') return <Prev {...rest} />
14
+
15
+ return (
16
+ <Prev
17
+ {...rest}
18
+ priceModifiers={[
19
+ ...(rest.priceModifiers ?? []),
20
+ ...filterNonNullableKeys(rest.item.bundle_options).map((option) => ({
21
+ key: option.uid,
22
+ label: option.label,
23
+ items: filterNonNullableKeys(option.values).map((value, index) => ({
24
+ key: `${index}`,
25
+ label: value.product_name,
26
+ amount: value.price.value ?? 0,
27
+ quantity: value.quantity,
28
+ })),
29
+ })),
30
+ ]}
31
+ />
32
+ )
33
+ }
@@ -0,0 +1,117 @@
1
+ import { useWatch } from '@graphcommerce/ecommerce-ui'
2
+ import {
3
+ useFormAddProductsToCart,
4
+ type AddToCartItemSelector,
5
+ type ProductPagePriceProps,
6
+ } from '@graphcommerce/magento-product'
7
+ import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
8
+ import { filterNonNullableKeys, nonNullable } from '@graphcommerce/next-ui'
9
+ import {
10
+ calculateBundleOptionValuePrice,
11
+ substractCalculatedBundleOptionValuePrices,
12
+ sumCalculatedBundleOptionValuePrices,
13
+ toProductListPriceFragment,
14
+ type CalculatedBundleOptionValuePrice,
15
+ } from '../components/BundleProductOptions/calculateBundleOptionValuePrice'
16
+ import type { BundleProductItemOptionType } from '../components/BundleProductOptions/types'
17
+ import type { ProductPageItem_BundleFragment } from '../graphql'
18
+
19
+ export const config: PluginConfig = {
20
+ type: 'component',
21
+ module: '@graphcommerce/magento-product',
22
+ }
23
+
24
+ function isBundleProduct(
25
+ product:
26
+ | ProductPagePriceProps['product']
27
+ | (ProductPagePriceProps['product'] & ProductPageItem_BundleFragment),
28
+ ): product is ProductPagePriceProps['product'] & ProductPageItem_BundleFragment {
29
+ return (
30
+ product.__typename === 'BundleProduct' &&
31
+ Array.isArray((product as ProductPageItem_BundleFragment).items)
32
+ )
33
+ }
34
+
35
+ export function ProductPagePrice(
36
+ props: PluginProps<ProductPagePriceProps> & AddToCartItemSelector,
37
+ ) {
38
+ const { Prev, product, index = 0, ...rest } = props
39
+
40
+ const form = useFormAddProductsToCart()
41
+ const allSelectedOptions =
42
+ useWatch({
43
+ control: form.control,
44
+ name: `cartItems.${index}.selected_options_record`,
45
+ }) ?? {}
46
+
47
+ const allEnteredOptions =
48
+ useWatch({
49
+ control: form.control,
50
+ name: `cartItems.${index}.entered_options_record`,
51
+ }) ?? {}
52
+
53
+ if (!isBundleProduct(product)) {
54
+ return <Prev product={product} index={index} {...rest} />
55
+ }
56
+
57
+ const cheapestPricesAlreadyIncludedInBasePrice = filterNonNullableKeys(product.items)
58
+ .filter((item) => item.required)
59
+ .map((item) =>
60
+ item.options
61
+ .filter(nonNullable)
62
+ .map((option) => calculateBundleOptionValuePrice(product, item, option))
63
+ .reduce((acc, price) => (price[1] < acc[1] ? price : acc)),
64
+ )
65
+
66
+ const reduceBase = sumCalculatedBundleOptionValuePrices(cheapestPricesAlreadyIncludedInBasePrice)
67
+
68
+ const basePrice: CalculatedBundleOptionValuePrice = [
69
+ product.price_range.minimum_price.regular_price.value ?? 0,
70
+ product.price_range.minimum_price.final_price.value ?? 0,
71
+ ]
72
+ const base = substractCalculatedBundleOptionValuePrices(basePrice, reduceBase)
73
+
74
+ // This only works with Magento 2.4.7, but that is fine.
75
+ const itemPrices = filterNonNullableKeys(product.items)
76
+ .map((item) => {
77
+ const selectedOption = allSelectedOptions[item.uid]
78
+ const allOptions = item.options.filter(nonNullable)
79
+
80
+ const options: BundleProductItemOptionType[] = selectedOption
81
+ ? allOptions.filter((o) => {
82
+ if (Array.isArray(selectedOption)) return selectedOption.includes(o?.uid ?? '')
83
+ return selectedOption === o?.uid
84
+ })
85
+ : allOptions.filter((o) => o?.is_default)
86
+
87
+ return options.map((option) => {
88
+ const quantity = allEnteredOptions[option.uid]
89
+ ? Number(allEnteredOptions[option.uid])
90
+ : (option.quantity ?? 1)
91
+ return calculateBundleOptionValuePrice(product, item, option, quantity)
92
+ })
93
+ })
94
+ .flat(1)
95
+
96
+ const totalPrice = toProductListPriceFragment(
97
+ sumCalculatedBundleOptionValuePrices([base, ...itemPrices]),
98
+ product.price_range.minimum_price.final_price.currency,
99
+ )
100
+
101
+ return (
102
+ <Prev
103
+ product={{
104
+ ...product,
105
+ price_range: {
106
+ ...product.price_range,
107
+ minimum_price: {
108
+ ...product.price_range.minimum_price,
109
+ ...totalPrice,
110
+ },
111
+ },
112
+ }}
113
+ index={index}
114
+ {...rest}
115
+ />
116
+ )
117
+ }
@@ -0,0 +1,33 @@
1
+ import type { ShipmentItemProps } from '@graphcommerce/magento-customer'
2
+ import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
4
+
5
+ export const config: PluginConfig = {
6
+ type: 'component',
7
+ module: '@graphcommerce/magento-customer',
8
+ }
9
+
10
+ export function ShipmentItem(props: PluginProps<ShipmentItemProps>) {
11
+ const { Prev, ...rest } = props
12
+
13
+ if (rest.item.__typename !== 'BundleShipmentItem') return <Prev {...rest} />
14
+
15
+ return (
16
+ <Prev
17
+ {...rest}
18
+ priceModifiers={[
19
+ ...(rest.priceModifiers ?? []),
20
+ ...filterNonNullableKeys(rest.item.bundle_options).map((option) => ({
21
+ key: option.uid,
22
+ label: option.label,
23
+ items: filterNonNullableKeys(option.values).map((value, index) => ({
24
+ key: `${index}`,
25
+ label: value.product_name,
26
+ amount: value.price.value ?? 0,
27
+ quantity: value.quantity,
28
+ })),
29
+ })),
30
+ ]}
31
+ />
32
+ )
33
+ }
@@ -0,0 +1,48 @@
1
+ import { type cartItemToCartItemInput as cartItemToCartItemInputType } from '@graphcommerce/magento-cart-items'
2
+ import type { AddProductsToCartFields } from '@graphcommerce/magento-product/components'
3
+ import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
4
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
5
+ import { toBundleOptionType } from '../components/BundleProductOptions/types'
6
+
7
+ export const config: PluginConfig = {
8
+ type: 'function',
9
+ module: '@graphcommerce/magento-cart-items',
10
+ }
11
+
12
+ export const cartItemToCartItemInput: FunctionPlugin<typeof cartItemToCartItemInputType> = (
13
+ prev,
14
+ props,
15
+ ) => {
16
+ const result = prev(props)
17
+ const { product, cartItem } = props
18
+
19
+ if (!result) return result
20
+ if (product.__typename !== 'BundleProduct') return result
21
+ if (cartItem.__typename !== 'BundleCartItem') return result
22
+
23
+ const selected: AddProductsToCartFields['cartItems'][number]['selected_options_record'] = {}
24
+ const entered: AddProductsToCartFields['cartItems'][number]['entered_options_record'] = {}
25
+
26
+ const items = filterNonNullableKeys(product.items)
27
+
28
+ filterNonNullableKeys(cartItem.bundle_options).forEach((option) => {
29
+ const values = filterNonNullableKeys(option?.values)
30
+ const productItem = items.find((item) => item.uid === option.uid)
31
+ const type = toBundleOptionType(productItem?.type)
32
+
33
+ const vals = values.map((v) => v.uid)
34
+ selected[option.uid] = type === 'multi' || type === 'checkbox' ? vals : vals[0]
35
+
36
+ values.forEach((v) => {
37
+ const productOptions = filterNonNullableKeys(productItem?.options)
38
+ const productOption = productOptions.find((o) => o.uid === v.uid)
39
+ if (productOption?.can_change_quantity) entered[v.uid] = v.quantity
40
+ })
41
+ })
42
+
43
+ return {
44
+ ...result,
45
+ selected_options_record: { ...result.selected_options_record, ...selected },
46
+ entered_options_record: { ...result.entered_options_record, ...entered },
47
+ }
48
+ }
@@ -1,3 +0,0 @@
1
- query BundleProductPage($urlKey: String) {
2
- ...ProductPageBundleQueryFragment
3
- }
@@ -1,9 +0,0 @@
1
- fragment ProductPageBundleQueryFragment on Query {
2
- typeProducts: products(filter: { url_key: { eq: $urlKey } }) {
3
- items {
4
- __typename
5
- uid
6
- ...BundleProductOptions
7
- }
8
- }
9
- }
@@ -1,36 +0,0 @@
1
- import { SelectedCustomizableOptions } from '@graphcommerce/magento-cart-items'
2
- import { Money } from '@graphcommerce/magento-store'
3
- import { Typography } from '@mui/material'
4
- import type { BundleProductCartItemOptionsProps } from '../BundleProductCartItemOptions/BundleProductCartItemOptions'
5
-
6
- /**
7
- * @deprecated
8
- * @public
9
- */
10
- export function BundleCartItem(props: BundleProductCartItemOptionsProps) {
11
- const { bundle_options } = props
12
- return (
13
- <>
14
- {bundle_options.map((option) => {
15
- if (!option?.uid) return null
16
- return (
17
- <div key={option.uid}>
18
- {option.values.map((value) => {
19
- if (!value?.uid) return null
20
- return (
21
- <Typography variant='body2' component='div' key={value.uid}>
22
- {value.quantity > 1 && <>{value.quantity} &times; </>}
23
- <Typography variant='subtitle2' component='span'>
24
- {value.label}
25
- </Typography>{' '}
26
- {value.price > 0 && <Money value={value.price} />}
27
- </Typography>
28
- )
29
- })}
30
- </div>
31
- )
32
- })}
33
- <SelectedCustomizableOptions {...props} />
34
- </>
35
- )
36
- }
@@ -1,42 +0,0 @@
1
- import type { CartItemFragment } from '@graphcommerce/magento-cart-items'
2
- import { SelectedCustomizableOptions } from '@graphcommerce/magento-cart-items'
3
- import { Money } from '@graphcommerce/magento-store'
4
- import { nonNullable } from '@graphcommerce/next-ui'
5
- import { Box } from '@mui/material'
6
- import type { BundleCartItemFragment } from '../BundleCartItem/BundleCartItem.gql'
7
-
8
- export type BundleProductCartItemOptionsProps = BundleCartItemFragment & CartItemFragment
9
-
10
- export function BundleProductCartItemOptions(props: BundleProductCartItemOptionsProps) {
11
- const { bundle_options, prices } = props
12
-
13
- return (
14
- <>
15
- {bundle_options?.map((option) =>
16
- option?.values.filter(nonNullable).map((option_value) => (
17
- <Box
18
- key={option_value.uid}
19
- sx={(theme) => ({
20
- display: 'flex',
21
- gap: theme.spacings.xxs,
22
- [theme.breakpoints.down('sm')]: {
23
- fontSize: theme.typography.caption.fontSize,
24
- },
25
- })}
26
- >
27
- <Box sx={{ color: 'text.primary' }}>
28
- {option_value.label} {option_value.quantity > 1 && `x${option_value.quantity}`}
29
- </Box>
30
- {option_value.price > 0 && (
31
- <Box sx={(theme) => ({ position: 'absolute', right: theme.spacings.xs })}>
32
- <Money currency={prices?.price.currency} value={option_value.price} />
33
- </Box>
34
- )}
35
- </Box>
36
- )),
37
- )}
38
-
39
- <SelectedCustomizableOptions {...props} />
40
- </>
41
- )
42
- }