@graphcommerce/magento-product-configurable 9.0.0-canary.114 → 9.0.0-canary.116

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,9 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.0.0-canary.116
4
+
5
+ ## 9.0.0-canary.115
6
+
3
7
  ## 9.0.0-canary.114
4
8
 
5
9
  ## 9.0.0-canary.113
@@ -3,6 +3,10 @@ import { CartItem } from '@graphcommerce/magento-cart-items'
3
3
  import type { ConfigurableCartItemFragment } from './ConfigurableCartItem.gql'
4
4
  import { OptionsList } from './OptionsList'
5
5
 
6
+ /**
7
+ * @deprecated
8
+ * @public
9
+ */
6
10
  export function ConfigurableCartItem(props: ConfigurableCartItemFragment & CartItemProps) {
7
11
  const { configurable_options, configured_variant, product } = props
8
12
  return (
package/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  export * from './components'
2
2
  export * from './ConfigurableCartItem/ConfigurableCartItem'
3
- export * from './ConfigurableContext/ConfigurableContext'
4
- export * from './ConfigurableProductAddToCart/ConfigurableProductAddToCart'
5
3
  export * from './ConfigurableProductPage.gql'
6
4
  export * from './graphql'
7
5
  export * from './hooks'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-product-configurable",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "9.0.0-canary.114",
5
+ "version": "9.0.0-canary.116",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -12,23 +12,23 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@graphcommerce/ecommerce-ui": "^9.0.0-canary.114",
16
- "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.114",
17
- "@graphcommerce/graphql": "^9.0.0-canary.114",
18
- "@graphcommerce/graphql-mesh": "^9.0.0-canary.114",
19
- "@graphcommerce/image": "^9.0.0-canary.114",
20
- "@graphcommerce/lingui-next": "9.0.0-canary.114",
21
- "@graphcommerce/magento-cart": "^9.0.0-canary.114",
22
- "@graphcommerce/magento-cart-items": "^9.0.0-canary.114",
23
- "@graphcommerce/magento-category": "^9.0.0-canary.114",
24
- "@graphcommerce/magento-customer": "^9.0.0-canary.114",
25
- "@graphcommerce/magento-product": "^9.0.0-canary.114",
26
- "@graphcommerce/magento-product-simple": "^9.0.0-canary.114",
27
- "@graphcommerce/magento-store": "^9.0.0-canary.114",
28
- "@graphcommerce/next-ui": "^9.0.0-canary.114",
29
- "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.114",
30
- "@graphcommerce/react-hook-form": "^9.0.0-canary.114",
31
- "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.114",
15
+ "@graphcommerce/ecommerce-ui": "^9.0.0-canary.116",
16
+ "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.116",
17
+ "@graphcommerce/graphql": "^9.0.0-canary.116",
18
+ "@graphcommerce/graphql-mesh": "^9.0.0-canary.116",
19
+ "@graphcommerce/image": "^9.0.0-canary.116",
20
+ "@graphcommerce/lingui-next": "9.0.0-canary.116",
21
+ "@graphcommerce/magento-cart": "^9.0.0-canary.116",
22
+ "@graphcommerce/magento-cart-items": "^9.0.0-canary.116",
23
+ "@graphcommerce/magento-category": "^9.0.0-canary.116",
24
+ "@graphcommerce/magento-customer": "^9.0.0-canary.116",
25
+ "@graphcommerce/magento-product": "^9.0.0-canary.116",
26
+ "@graphcommerce/magento-product-simple": "^9.0.0-canary.116",
27
+ "@graphcommerce/magento-store": "^9.0.0-canary.116",
28
+ "@graphcommerce/next-ui": "^9.0.0-canary.116",
29
+ "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.116",
30
+ "@graphcommerce/react-hook-form": "^9.0.0-canary.116",
31
+ "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.116",
32
32
  "@lingui/core": "^4.2.1",
33
33
  "@lingui/macro": "^4.2.1",
34
34
  "@lingui/react": "^4.2.1",
@@ -1,157 +0,0 @@
1
- import type { Context, Dispatch, SetStateAction } from 'react'
2
- import { createContext, useCallback, useContext, useMemo, useState } from 'react'
3
- import type { ConfigurableProductFormFragment } from './ConfigurableProductForm.gql'
4
- import cheapestVariant from './cheapestVariant'
5
-
6
- export type ConfigurableProductFormProps = ConfigurableProductFormFragment & {
7
- sku: string
8
- children?: React.ReactNode
9
- }
10
-
11
- export type Selected = { [attrCode: string]: number }
12
- export type Variants = NonNullable<ConfigurableProductFormProps['variants']>
13
- type GetVariants = (values?: Selected) => Variants
14
- type GetUids = (values?: Selected) => string[]
15
-
16
- type ConfigurableContext = {
17
- selection: Selected
18
- variants: Variants
19
- cheapest: Variants[0]
20
- select: Dispatch<SetStateAction<Selected>>
21
- options: ConfigurableProductFormFragment['configurable_options']
22
- getVariants: GetVariants
23
- getUids: GetUids
24
- }
25
- const contexts: { [sku: string]: Context<ConfigurableContext> } = {}
26
-
27
- function configurableContext(sku: string): Context<ConfigurableContext> {
28
- if (contexts?.[sku]) return contexts[sku]
29
- contexts[sku] = createContext<ConfigurableContext>({
30
- selection: {},
31
- variants: [],
32
- cheapest: {},
33
- select: () => {},
34
- options: undefined,
35
- getVariants: () => [],
36
- getUids: () => [],
37
- })
38
-
39
- return contexts[sku]
40
- }
41
-
42
- type AttributeTree = {
43
- code: string
44
- values: AttributeValues
45
- }
46
- type AttributeValues = {
47
- [index: string]: {
48
- variants: NonNullable<ConfigurableProductFormProps['variants']>
49
- attribute?: AttributeTree
50
- }
51
- }
52
-
53
- function generateAttrTree(
54
- idx: number,
55
- options: ConfigurableProductFormProps['configurable_options'],
56
- variants: ConfigurableProductFormProps['variants'],
57
- selected: Selected,
58
- tree?: AttributeTree,
59
- ) {
60
- const attribute = options?.[idx]
61
- if (!attribute || !attribute.attribute_code) return tree
62
-
63
- const attributeTree: AttributeTree = { code: attribute.attribute_code, values: {} }
64
-
65
- attribute.values?.forEach((val) => {
66
- if (!val?.uid) return
67
- const newSelected = { ...selected, [attributeTree.code]: [val.uid] } as Selected
68
-
69
- const filteredVariants = variants?.filter(
70
- (variant) =>
71
- !!variant?.attributes?.find(
72
- (attr) => attr?.code === attribute.attribute_code && val.uid === attr?.uid,
73
- ),
74
- )
75
-
76
- attributeTree.values[val.uid] = {
77
- variants: filteredVariants ?? [],
78
- attribute: generateAttrTree(idx + 1, options, filteredVariants, newSelected),
79
- }
80
- })
81
-
82
- return attributeTree
83
- }
84
-
85
- function traverseAttrTree(selection: Selected, attrTree: AttributeTree | undefined): Variants {
86
- if (!attrTree) return []
87
-
88
- const id = selection?.[attrTree.code]
89
- const attrVal = id ? attrTree.values[id] : undefined
90
-
91
- // We have a request, but isn't found in the current tree node
92
- if (id && !attrVal) return []
93
-
94
- if (attrVal?.attribute) return traverseAttrTree(selection, attrVal.attribute)
95
- if (attrVal?.variants) return attrVal.variants
96
-
97
- const attrValues = Object.entries(attrTree.values)
98
- const variantList: NonNullable<ConfigurableProductFormProps['variants']> = []
99
-
100
- attrValues.forEach(([optionId, attrVal2]) => {
101
- variantList.push(
102
- ...(attrVal2.attribute
103
- ? traverseAttrTree({ ...selection, [attrTree.code]: Number(optionId) }, attrVal2.attribute)
104
- : attrVal2.variants),
105
- )
106
- })
107
-
108
- return variantList
109
- }
110
-
111
- export function ConfigurableContextProvider(props: ConfigurableProductFormProps) {
112
- const { children, sku, configurable_options, variants: providedVariants } = props
113
- const [selection, select] = useState<Selected>({})
114
-
115
- if (!configurable_options || !providedVariants)
116
- throw Error('please provide configurabl_options and variants')
117
-
118
- const lookupTree = useMemo(
119
- () => generateAttrTree(0, configurable_options, providedVariants, {}),
120
- [configurable_options, providedVariants],
121
- )
122
-
123
- const getVariants: GetVariants = useCallback(
124
- (options: Selected = {}) => traverseAttrTree(options, lookupTree),
125
- [lookupTree],
126
- )
127
-
128
- const getUids: GetUids = useCallback(
129
- (options: Selected = {}) =>
130
- (getVariants(options as unknown as Selected) ?? [])
131
- .map((variant) => (variant?.attributes?.map((attr) => attr?.uid) ?? []) as string[])
132
- .flat(),
133
- [getVariants],
134
- )
135
-
136
- const context = configurableContext(sku)
137
- const variants = getVariants(selection)
138
-
139
- const value = useMemo(
140
- () => ({
141
- selection,
142
- variants,
143
- cheapest: cheapestVariant(variants),
144
- select,
145
- getVariants,
146
- getUids,
147
- options: configurable_options,
148
- }),
149
- [configurable_options, getUids, getVariants, selection, variants],
150
- )
151
-
152
- return <context.Provider value={value}>{children}</context.Provider>
153
- }
154
-
155
- export function useConfigurableContext(sku: string): ConfigurableContext {
156
- return useContext(configurableContext(sku))
157
- }
@@ -1,14 +0,0 @@
1
- import type { ConfigurableProductFormFragment } from './ConfigurableProductForm.gql'
2
-
3
- type Variants = NonNullable<ConfigurableProductFormFragment['variants']>
4
-
5
- export default function cheapestVariant(variants: Variants): Variants[0] {
6
- if (!variants.length) return null
7
- const cheapest = variants?.reduce((prev, curr) =>
8
- (curr?.product?.price_range.minimum_price.final_price.value ?? 0) <
9
- (prev?.product?.price_range.minimum_price.final_price.value ?? 0)
10
- ? curr
11
- : prev,
12
- )
13
- return cheapest
14
- }
@@ -1,146 +0,0 @@
1
- import type { FieldErrors, UseControllerProps } from '@graphcommerce/ecommerce-ui'
2
- import { Controller } from '@graphcommerce/ecommerce-ui'
3
- import {
4
- RenderType,
5
- SectionHeader,
6
- ToggleButton,
7
- ToggleButtonGroup,
8
- extendableComponent,
9
- } from '@graphcommerce/next-ui'
10
- import type { BaseTextFieldProps, SxProps } from '@mui/material'
11
- import { FormHelperText } from '@mui/material'
12
- import React from 'react'
13
- import type { Selected } from '../ConfigurableContext/ConfigurableContext'
14
- import { useConfigurableContext } from '../ConfigurableContext/ConfigurableContext'
15
- import { ColorSwatchData } from '../Swatches/ColorSwatchData'
16
- import { ImageSwatchData } from '../Swatches/ImageSwatchData'
17
- import { TextSwatchData } from '../Swatches/TextSwatchData'
18
- import type { SwatchSize, SwatchTypeRenderer } from '../Swatches/types'
19
-
20
- export type ConfigurableOptionsInputProps = {
21
- sku: string
22
- errors?: FieldErrors
23
- size?: SwatchSize
24
- sx?: SxProps
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- } & UseControllerProps<any> &
27
- Pick<BaseTextFieldProps, 'FormHelperTextProps' | 'helperText'> & {
28
- optionEndLabels?: Record<string, React.ReactNode>
29
- }
30
-
31
- const renderer: SwatchTypeRenderer = { TextSwatchData, ImageSwatchData, ColorSwatchData }
32
-
33
- const compName = 'ConfigurableOptionsInput'
34
- const parts = ['buttonGroup', 'button', 'helperText'] as const
35
- const { classes } = extendableComponent(compName, parts)
36
-
37
- export function ConfigurableOptionsInput(props: ConfigurableOptionsInputProps) {
38
- const {
39
- sku,
40
- FormHelperTextProps,
41
- name,
42
- defaultValue,
43
- errors,
44
- helperText,
45
- optionEndLabels,
46
- size = 'large',
47
- sx,
48
- ...controlProps
49
- } = props
50
-
51
- const { options, selection, select, getVariants } = useConfigurableContext(sku)
52
-
53
- return (
54
- <>
55
- {options?.map((option) => {
56
- if (!option?.uid || !option.attribute_code) return null
57
-
58
- const { attribute_code } = option
59
- const error = errors?.[attribute_code]
60
-
61
- return (
62
- <Controller
63
- key={option.uid}
64
- defaultValue={selection[attribute_code] ?? ''}
65
- name={`${name}[${attribute_code}]`}
66
- {...controlProps}
67
- render={({
68
- field: { onChange, value, name: inputName, ref, onBlur },
69
- fieldState: { error: errorHelperText },
70
- }) => (
71
- <>
72
- <SectionHeader
73
- labelLeft={option?.label}
74
- labelRight={optionEndLabels?.[option?.attribute_code ?? '']}
75
- />
76
- <ToggleButtonGroup
77
- defaultValue={selection[attribute_code] ?? ''}
78
- required
79
- exclusive
80
- onChange={(_, val: string | number) => {
81
- onChange(val)
82
- select((prev) => ({ ...prev, [attribute_code]: val }) as Selected)
83
- }}
84
- ref={ref}
85
- onBlur={onBlur}
86
- value={value}
87
- className={classes.buttonGroup}
88
- size={size}
89
- sx={sx}
90
- >
91
- {option?.values?.map((val) => {
92
- if (!val?.uid || !option.attribute_code) return null
93
-
94
- // Fall back to text swatch if no swatch is given
95
- const swatch_data = val.swatch_data ?? {
96
- __typename: 'TextSwatchData',
97
- value: val.store_label,
98
- }
99
-
100
- const copySelection = { ...selection }
101
- delete copySelection[attribute_code]
102
-
103
- const itemVariant = getVariants(copySelection).find((variant) =>
104
- variant?.attributes?.find((attribute) => attribute?.uid === val.uid),
105
- )
106
-
107
- return (
108
- <ToggleButton
109
- key={val.uid}
110
- value={val.uid ?? ''}
111
- name={inputName}
112
- className={classes.button}
113
- disabled={!itemVariant}
114
- size={size}
115
- >
116
- <RenderType
117
- renderer={renderer}
118
- {...val}
119
- {...swatch_data}
120
- price={itemVariant?.product?.price_range.minimum_price.final_price}
121
- size={size}
122
- />
123
- </ToggleButton>
124
- )
125
- })}
126
- </ToggleButtonGroup>
127
- {error && (
128
- <FormHelperText
129
- error
130
- {...FormHelperTextProps}
131
- className={classes.helperText}
132
- sx={{
133
- position: 'absolute',
134
- }}
135
- >
136
- {`${option.label} is ${errorHelperText?.type}`}
137
- </FormHelperText>
138
- )}
139
- </>
140
- )}
141
- />
142
- )
143
- })}
144
- </>
145
- )
146
- }
@@ -1,182 +0,0 @@
1
- import { NumberFieldElement } from '@graphcommerce/ecommerce-ui'
2
- import { ApolloCartErrorAlert, useFormGqlMutationCart } from '@graphcommerce/magento-cart'
3
- import { Money } from '@graphcommerce/magento-store'
4
- import {
5
- Button,
6
- IconSvg,
7
- MessageSnackbar,
8
- extendableComponent,
9
- iconChevronRight,
10
- } from '@graphcommerce/next-ui'
11
- import { Trans } from '@lingui/macro'
12
- import type { SxProps, Theme } from '@mui/material'
13
- import { Alert, Box, Divider, Typography } from '@mui/material'
14
- import React from 'react'
15
- import { useConfigurableContext } from '../ConfigurableContext/ConfigurableContext'
16
- import cheapestVariant from '../ConfigurableContext/cheapestVariant'
17
- import type { ConfigurableOptionsInputProps } from '../ConfigurableOptions/ConfigurableOptions'
18
- import { ConfigurableOptionsInput } from '../ConfigurableOptions/ConfigurableOptions'
19
- import type { ConfigurableProductAddToCartMutationVariables } from '../graphql/ConfigurableProductAddToCart.gql'
20
- import { ConfigurableProductAddToCartDocument } from '../graphql/ConfigurableProductAddToCart.gql'
21
-
22
- export type ConfigurableProductAddToCartProps = {
23
- variables: Omit<ConfigurableProductAddToCartMutationVariables, 'cartId' | 'selectedOptions'>
24
- name: string
25
- optionEndLabels?: Record<string, React.ReactNode>
26
- children?: React.ReactNode
27
- additionalButtons?: React.ReactNode
28
- sx?: SxProps<Theme>
29
- optionsProps?: Omit<
30
- ConfigurableOptionsInputProps,
31
- 'name' | 'sku' | 'control' | 'rules' | 'errors' | 'optionEndLabels'
32
- >
33
- }
34
-
35
- const compName = 'ConfigurableOptionsInput'
36
- const parts = ['form', 'button', 'finalPrice', 'quantity', 'divider', 'buttonWrapper'] as const
37
- const { classes } = extendableComponent(compName, parts)
38
-
39
- /**
40
- * @deprecated
41
- */
42
- export function ConfigurableProductAddToCart(props: ConfigurableProductAddToCartProps) {
43
- const {
44
- name,
45
- children,
46
- variables,
47
- optionEndLabels,
48
- optionsProps,
49
- additionalButtons,
50
- sx = [],
51
- ...buttonProps
52
- } = props
53
-
54
- const { getVariants, selection } = useConfigurableContext(variables.sku)
55
-
56
- const form = useFormGqlMutationCart(ConfigurableProductAddToCartDocument, {
57
- defaultValues: { ...variables },
58
- onBeforeSubmit: ({ selectedOptions, ...vars }) => ({
59
- ...vars,
60
- selectedOptions: Object.values(selectedOptions),
61
- }),
62
- })
63
-
64
- const { handleSubmit, formState, required, control, error, data } = form
65
- const submitHandler = handleSubmit(() => {})
66
-
67
- return (
68
- <Box
69
- component='form'
70
- onSubmit={submitHandler}
71
- noValidate
72
- className={classes.form}
73
- sx={[
74
- {
75
- width: '100%',
76
- },
77
- ...(Array.isArray(sx) ? sx : [sx]),
78
- ]}
79
- >
80
- <Divider className={classes.divider} sx={(theme) => ({ margin: `${theme.spacings.sm} 0` })} />
81
- <ConfigurableOptionsInput
82
- name='selectedOptions'
83
- sku={variables.sku}
84
- control={control}
85
- rules={{ required: required.selectedOptions }}
86
- errors={formState.errors}
87
- optionEndLabels={optionEndLabels}
88
- {...optionsProps}
89
- />
90
-
91
- <NumberFieldElement
92
- variant='outlined'
93
- error={formState.isSubmitted && !!formState.errors.quantity}
94
- required={required.quantity}
95
- inputProps={{ min: 1 }}
96
- name='quantity'
97
- rules={{ required: required.quantity }}
98
- control={control}
99
- helperText={formState.isSubmitted && formState.errors.quantity?.message}
100
- // disabled={loading}
101
- size='small'
102
- className={classes.quantity}
103
- sx={(theme) => ({ marginTop: theme.spacings.sm })}
104
- />
105
- <Divider className={classes.divider} sx={(theme) => ({ margin: `${theme.spacings.sm} 0` })} />
106
- <Typography
107
- component='div'
108
- variant='h3'
109
- className={classes.finalPrice}
110
- sx={(theme) => ({ marginTop: theme.spacings.sm })}
111
- >
112
- <Money
113
- {...cheapestVariant(getVariants(selection))?.product?.price_range.minimum_price
114
- .final_price}
115
- />
116
- </Typography>
117
- {children}
118
- <Box
119
- sx={(theme) => ({
120
- display: 'flex',
121
- alignItems: 'center',
122
- columnGap: theme.spacings.xs,
123
- })}
124
- className={classes.buttonWrapper}
125
- >
126
- <Button
127
- type='submit'
128
- loading={formState.isSubmitting}
129
- color='primary'
130
- variant='pill'
131
- size='large'
132
- className={classes.button}
133
- {...buttonProps}
134
- sx={(theme) => ({
135
- marginTop: theme.spacings.sm,
136
- marginBottom: theme.spacings.sm,
137
- width: '100%',
138
- })}
139
- >
140
- <Trans>Add to Cart</Trans>
141
- </Button>
142
- {additionalButtons}
143
- </Box>
144
-
145
- <ApolloCartErrorAlert error={error} />
146
-
147
- {data?.addProductsToCart?.user_errors.map((e) => (
148
- <Box key={e?.code}>
149
- <Alert severity='error'>{e?.message}</Alert>
150
- </Box>
151
- ))}
152
-
153
- <MessageSnackbar
154
- open={
155
- !formState.isSubmitting &&
156
- formState.isSubmitSuccessful &&
157
- !error?.message &&
158
- !data?.addProductsToCart?.user_errors?.length
159
- }
160
- variant='pill'
161
- severity='success'
162
- autoHide
163
- action={
164
- <Button
165
- href='/cart'
166
- id='view-shopping-cart-button'
167
- size='medium'
168
- variant='pill'
169
- color='secondary'
170
- endIcon={<IconSvg src={iconChevronRight} />}
171
- >
172
- <Trans>View shopping cart</Trans>
173
- </Button>
174
- }
175
- >
176
- <Trans>
177
- <strong>{name}</strong> has been added to your shopping cart!
178
- </Trans>
179
- </MessageSnackbar>
180
- </Box>
181
- )
182
- }