@magento/peregrine 12.5.1-beta.1 → 12.6.0-alpha.1

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.
@@ -5,10 +5,12 @@ import mergeOperations from '../../util/shallowMerge';
5
5
  import { useCartContext } from '../../context/cart';
6
6
  import defaultOperations from './addToCartDialog.gql';
7
7
  import { useEventingContext } from '../../context/eventing';
8
+ import { isProductConfigurable } from '@magento/peregrine/lib/util/isProductConfigurable';
9
+ import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
8
10
 
9
11
  export const useAddToCartDialog = props => {
10
12
  const { item, onClose } = props;
11
- const sku = item && item.product.sku;
13
+ const sku = item && item.product?.sku;
12
14
 
13
15
  const [, { dispatch }] = useEventingContext();
14
16
 
@@ -18,9 +20,58 @@ export const useAddToCartDialog = props => {
18
20
  const [currentImage, setCurrentImage] = useState();
19
21
  const [currentPrice, setCurrentPrice] = useState();
20
22
  const [currentDiscount, setCurrentDiscount] = useState();
23
+ const [singleOptionSelection, setSingleOptionSelection] = useState();
24
+ const [multipleOptionSelections, setMultipleOptionSelections] = useState(
25
+ new Map()
26
+ );
21
27
 
22
28
  const [{ cartId }] = useCartContext();
23
29
 
30
+ const optionCodes = useMemo(() => {
31
+ const optionCodeMap = new Map();
32
+ if (item) {
33
+ item.product?.configurable_options.forEach(option => {
34
+ optionCodeMap.set(option.attribute_id, option.attribute_code);
35
+ });
36
+ }
37
+ return optionCodeMap;
38
+ }, [item]);
39
+
40
+ // Check if display out of stock products option is selected in the Admin Dashboard
41
+ const isOutOfStockProductDisplayed = useMemo(() => {
42
+ if (item) {
43
+ let totalVariants = 1;
44
+ const { product } = item;
45
+ const isConfigurable = isProductConfigurable(product);
46
+ if (product?.configurable_options && isConfigurable) {
47
+ for (const option of product.configurable_options) {
48
+ const length = option.values.length;
49
+ totalVariants = totalVariants * length;
50
+ }
51
+ return product.variants.length === totalVariants;
52
+ }
53
+ }
54
+ }, [item]);
55
+
56
+ const outOfStockVariants = useMemo(() => {
57
+ if (item) {
58
+ const product = item.product;
59
+ return getOutOfStockVariants(
60
+ product,
61
+ optionCodes,
62
+ singleOptionSelection,
63
+ multipleOptionSelections,
64
+ isOutOfStockProductDisplayed
65
+ );
66
+ }
67
+ }, [
68
+ item,
69
+ optionCodes,
70
+ singleOptionSelection,
71
+ multipleOptionSelections,
72
+ isOutOfStockProductDisplayed
73
+ ]);
74
+
24
75
  const selectedOptionsArray = useMemo(() => {
25
76
  if (item) {
26
77
  const existingOptionsMap = item.configurable_options.reduce(
@@ -39,14 +90,14 @@ export const useAddToCartDialog = props => {
39
90
 
40
91
  const selectedOptions = [];
41
92
  mergedOptionsMap.forEach((selectedValueId, attributeId) => {
42
- const configurableOption = item.product.configurable_options.find(
93
+ const configurableOption = item.product?.configurable_options.find(
43
94
  option => option.attribute_id_v2 === attributeId
44
95
  );
45
- const configurableOptionValue = configurableOption.values.find(
96
+ const configurableOptionValue = configurableOption?.values.find(
46
97
  optionValue => optionValue.value_index === selectedValueId
47
98
  );
48
99
 
49
- selectedOptions.push(configurableOptionValue.uid);
100
+ selectedOptions.push(configurableOptionValue?.uid);
50
101
  });
51
102
 
52
103
  return selectedOptions;
@@ -107,13 +158,27 @@ export const useAddToCartDialog = props => {
107
158
  setCurrentImage();
108
159
  setCurrentPrice();
109
160
  setUserSelectedOptions(new Map());
161
+ setMultipleOptionSelections(new Map());
110
162
  }, [onClose]);
111
163
 
112
- const handleOptionSelection = useCallback((optionId, value) => {
113
- setUserSelectedOptions(existing =>
114
- new Map(existing).set(parseInt(optionId), value)
115
- );
116
- }, []);
164
+ const handleOptionSelection = useCallback(
165
+ (optionId, value) => {
166
+ setUserSelectedOptions(existing =>
167
+ new Map(existing).set(parseInt(optionId), value)
168
+ );
169
+ // Create a new Map to keep track of user single selection with key as String
170
+ const nextSingleOptionSelection = new Map();
171
+ nextSingleOptionSelection.set(optionId, value);
172
+ setSingleOptionSelection(nextSingleOptionSelection);
173
+ // Create a new Map to keep track of multiple selections with key as String
174
+ const nextMultipleOptionSelections = new Map([
175
+ ...multipleOptionSelections
176
+ ]);
177
+ nextMultipleOptionSelections.set(optionId, value);
178
+ setMultipleOptionSelections(nextMultipleOptionSelections);
179
+ },
180
+ [multipleOptionSelections]
181
+ );
117
182
 
118
183
  const handleAddToCart = useCallback(async () => {
119
184
  try {
@@ -192,7 +257,7 @@ export const useAddToCartDialog = props => {
192
257
  if (item) {
193
258
  return {
194
259
  onSelectionChange: handleOptionSelection,
195
- options: item.product.configurable_options,
260
+ options: item.product?.configurable_options,
196
261
  selectedValues: item.configurable_options
197
262
  };
198
263
  }
@@ -202,7 +267,7 @@ export const useAddToCartDialog = props => {
202
267
  if (item) {
203
268
  return {
204
269
  disabled:
205
- item.product.configurable_options.length !==
270
+ item.product?.configurable_options.length !==
206
271
  selectedOptionsArray.length || isAddingToCart,
207
272
  onClick: handleAddToCart,
208
273
  priority: 'high'
@@ -215,6 +280,7 @@ export const useAddToCartDialog = props => {
215
280
  configurableOptionProps,
216
281
  formErrors: [addProductToCartError],
217
282
  handleOnClose,
283
+ outOfStockVariants,
218
284
  imageProps,
219
285
  isFetchingProductDetail,
220
286
  priceProps
@@ -52,6 +52,7 @@ export const ProductFormFragment = gql`
52
52
  }
53
53
  }
54
54
  sku
55
+ stock_status
55
56
  }
56
57
  }
57
58
  }
@@ -6,6 +6,7 @@ import { useCartContext } from '../../../../context/cart';
6
6
  import { findMatchingVariant } from '../../../../util/findMatchingProductVariant';
7
7
  import DEFAULT_OPERATIONS from './productForm.gql';
8
8
  import { useEventingContext } from '../../../../context/eventing';
9
+ import { getOutOfStockVariantsWithInitialSelection } from '../../../../util/getOutOfStockVariantsWithInitialSelection';
9
10
 
10
11
  /**
11
12
  * This talon contains logic for a product edit form.
@@ -32,6 +33,18 @@ import { useEventingContext } from '../../../../context/eventing';
32
33
  * @example <caption>Importing into your project</caption>
33
34
  * import { useProductForm } from '@magento/peregrine/lib/talons/CartPage/ProductListing/EditModal/useProductForm';
34
35
  */
36
+
37
+ // Get initial selections
38
+ function deriveOptionSelectionsFromProduct(cartItem) {
39
+ if (cartItem) {
40
+ const initialOptionSelections = new Map();
41
+ for (const { id, value_id } of cartItem.configurable_options) {
42
+ initialOptionSelections.set(String(id), value_id);
43
+ }
44
+ return initialOptionSelections;
45
+ }
46
+ }
47
+
35
48
  export const useProductForm = props => {
36
49
  const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
37
50
 
@@ -52,11 +65,28 @@ export const useProductForm = props => {
52
65
  const [, { dispatch }] = useEventingContext();
53
66
 
54
67
  const [{ cartId }] = useCartContext();
68
+
69
+ const derivedOptionSelections = useMemo(() => {
70
+ if (cartItem) {
71
+ return deriveOptionSelectionsFromProduct(cartItem);
72
+ }
73
+ }, [cartItem]);
74
+
55
75
  const [optionSelections, setOptionSelections] = useState(new Map());
76
+ const [multipleOptionSelections, setMultipleOptionSelections] = useState(
77
+ derivedOptionSelections ? derivedOptionSelections : new Map()
78
+ );
79
+ useEffect(() => {
80
+ if (cartItem) {
81
+ setMultipleOptionSelections(derivedOptionSelections);
82
+ }
83
+ }, [derivedOptionSelections, cartItem]);
56
84
 
57
85
  const handleClose = useCallback(() => {
86
+ setMultipleOptionSelections(new Map());
87
+ setOptionSelections(new Map());
58
88
  setActiveEditItem(null);
59
- }, [setActiveEditItem]);
89
+ }, [setActiveEditItem, setMultipleOptionSelections, setOptionSelections]);
60
90
 
61
91
  const [
62
92
  updateItemQuantity,
@@ -106,20 +136,41 @@ export const useProductForm = props => {
106
136
  option => option.id == optionId
107
137
  );
108
138
 
109
- if (initialSelection.value_id === selection) {
139
+ if (initialSelection?.value_id === selection) {
110
140
  nextOptionSelections.delete(optionId);
111
141
  } else {
112
142
  nextOptionSelections.set(optionId, selection);
113
143
  }
114
144
 
115
145
  setOptionSelections(nextOptionSelections);
146
+
147
+ // Create a new Map to only keep track of user multiple selections with key as String
148
+ // without considering initialSelection.value_id
149
+ const nextMultipleOptionSelections = new Map([
150
+ ...multipleOptionSelections
151
+ ]);
152
+ nextMultipleOptionSelections.set(optionId, selection);
153
+ setMultipleOptionSelections(nextMultipleOptionSelections);
116
154
  },
117
- [cartItem, optionSelections]
155
+ [cartItem, optionSelections, multipleOptionSelections]
118
156
  );
119
157
 
120
158
  const configItem =
121
159
  !loading && !error && data ? data.products.items[0] : null;
122
160
 
161
+ // Check if display out of stock products option is selected in the Admin Dashboard
162
+ const isOutOfStockProductDisplayed = useMemo(() => {
163
+ let totalVariants = 1;
164
+
165
+ if (configItem && configItem.configurable_options) {
166
+ for (const option of configItem.configurable_options) {
167
+ const length = option.values.length;
168
+ totalVariants = totalVariants * length;
169
+ }
170
+ return configItem.variants.length === totalVariants;
171
+ }
172
+ }, [configItem]);
173
+
123
174
  const configurableOptionCodes = useMemo(() => {
124
175
  const optionCodeMap = new Map();
125
176
 
@@ -149,6 +200,25 @@ export const useProductForm = props => {
149
200
  }
150
201
  }, [cartItem, configItem, configurableOptionCodes, optionSelections]);
151
202
 
203
+ const outOfStockVariants = useMemo(() => {
204
+ if (cartItem && configItem) {
205
+ const product = cartItem.product;
206
+ return getOutOfStockVariantsWithInitialSelection(
207
+ product,
208
+ configurableOptionCodes,
209
+ multipleOptionSelections,
210
+ configItem,
211
+ isOutOfStockProductDisplayed
212
+ );
213
+ }
214
+ }, [
215
+ cartItem,
216
+ configurableOptionCodes,
217
+ multipleOptionSelections,
218
+ configItem,
219
+ isOutOfStockProductDisplayed
220
+ ]);
221
+
152
222
  const configurableThumbnailSource = useMemo(() => {
153
223
  return storeConfigData?.storeConfig?.configurable_thumbnail_source;
154
224
  }, [storeConfigData]);
@@ -164,7 +234,10 @@ export const useProductForm = props => {
164
234
  try {
165
235
  const quantity = formValues.quantity;
166
236
 
167
- if (selectedVariant && optionSelections.size) {
237
+ if (
238
+ (selectedVariant && optionSelections.size) ||
239
+ (selectedVariant && multipleOptionSelections.size)
240
+ ) {
168
241
  await updateConfigurableOptions({
169
242
  variables: {
170
243
  cartId,
@@ -176,6 +249,7 @@ export const useProductForm = props => {
176
249
  });
177
250
 
178
251
  setOptionSelections(new Map());
252
+ setMultipleOptionSelections(new Map());
179
253
  } else if (quantity !== cartItem.quantity) {
180
254
  await updateItemQuantity({
181
255
  variables: {
@@ -234,6 +308,7 @@ export const useProductForm = props => {
234
308
  dispatch,
235
309
  handleClose,
236
310
  optionSelections.size,
311
+ multipleOptionSelections.size,
237
312
  selectedVariant,
238
313
  updateConfigurableOptions,
239
314
  updateItemQuantity
@@ -254,6 +329,7 @@ export const useProductForm = props => {
254
329
  errors,
255
330
  handleOptionSelection,
256
331
  handleSubmit,
332
+ outOfStockVariants,
257
333
  isLoading: !!loading,
258
334
  isSaving,
259
335
  isDialogOpen: cartItem !== null,
@@ -24,10 +24,13 @@ export const ProductListingFragment = gql`
24
24
  variants {
25
25
  attributes {
26
26
  uid
27
+ code
28
+ value_index
27
29
  }
28
30
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
29
31
  product {
30
32
  uid
33
+ stock_status
31
34
  small_image {
32
35
  url
33
36
  }
@@ -61,6 +64,7 @@ export const ProductListingFragment = gql`
61
64
  option_label
62
65
  configurable_product_option_value_uid
63
66
  value_label
67
+ value_id
64
68
  }
65
69
  }
66
70
  }
@@ -8,10 +8,33 @@ export const GET_PAYMENT_METHODS = gql`
8
8
  code
9
9
  title
10
10
  }
11
+ selected_payment_method {
12
+ code
13
+ }
14
+ }
15
+ }
16
+ `;
17
+
18
+ export const SET_PAYMENT_METHOD_ON_CART = gql`
19
+ mutation setPaymentMethodOnCart(
20
+ $cartId: String!
21
+ $paymentMethod: PaymentMethodInput!
22
+ ) {
23
+ setPaymentMethodOnCart(
24
+ input: { cart_id: $cartId, payment_method: $paymentMethod }
25
+ ) {
26
+ cart {
27
+ id
28
+ selected_payment_method {
29
+ code
30
+ title
31
+ }
32
+ }
11
33
  }
12
34
  }
13
35
  `;
14
36
 
15
37
  export default {
16
- getPaymentMethodsQuery: GET_PAYMENT_METHODS
38
+ getPaymentMethodsQuery: GET_PAYMENT_METHODS,
39
+ setPaymentMethodOnCartMutation: SET_PAYMENT_METHOD_ON_CART
17
40
  };
@@ -1,4 +1,5 @@
1
- import { useQuery } from '@apollo/client';
1
+ import { useCallback } from 'react';
2
+ import { useMutation, useQuery } from '@apollo/client';
2
3
  import useFieldState from '@magento/peregrine/lib/hooks/hook-wrappers/useInformedFieldStateWrapper';
3
4
  import DEFAULT_OPERATIONS from './paymentMethods.gql';
4
5
  import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
@@ -7,7 +8,12 @@ import { useCartContext } from '../../../context/cart';
7
8
 
8
9
  export const usePaymentMethods = props => {
9
10
  const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
10
- const { getPaymentMethodsQuery } = operations;
11
+ const {
12
+ getPaymentMethodsQuery,
13
+ setPaymentMethodOnCartMutation
14
+ } = operations;
15
+
16
+ const [setPaymentMethod] = useMutation(setPaymentMethodOnCartMutation);
11
17
 
12
18
  const [{ cartId }] = useCartContext();
13
19
 
@@ -23,13 +29,43 @@ export const usePaymentMethods = props => {
23
29
  const availablePaymentMethods =
24
30
  (data && data.cart.available_payment_methods) || [];
25
31
 
26
- const initialSelectedMethod =
32
+ // If there is one payment method, select it by default.
33
+ // If more than one, none should be selected by default.
34
+ const defaultPaymentCode =
27
35
  (availablePaymentMethods.length && availablePaymentMethods[0].code) ||
28
36
  null;
37
+ const selectedPaymentCode =
38
+ (data && data.cart.selected_payment_method.code) || null;
39
+
40
+ const initialSelectedMethod =
41
+ availablePaymentMethods.length > 1
42
+ ? selectedPaymentCode
43
+ : defaultPaymentCode;
44
+
45
+ const handlePaymentMethodSelection = useCallback(
46
+ element => {
47
+ const value = element.target.value;
48
+
49
+ setPaymentMethod({
50
+ variables: {
51
+ cartId,
52
+ paymentMethod: {
53
+ code: value,
54
+ braintree: {
55
+ payment_method_nonce: value,
56
+ is_active_payment_token_enabler: false
57
+ }
58
+ }
59
+ }
60
+ });
61
+ },
62
+ [cartId, setPaymentMethod]
63
+ );
29
64
 
30
65
  return {
31
66
  availablePaymentMethods,
32
67
  currentSelectedPaymentMethod,
68
+ handlePaymentMethodSelection,
33
69
  initialSelectedMethod,
34
70
  isLoading: loading
35
71
  };
@@ -12,10 +12,12 @@ import { deriveErrorMessage } from '../../util/deriveErrorMessage';
12
12
  import mergeOperations from '../../util/shallowMerge';
13
13
  import defaultOperations from './productFullDetail.gql';
14
14
  import { useEventingContext } from '../../context/eventing';
15
+ import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
15
16
 
16
17
  const INITIAL_OPTION_CODES = new Map();
17
18
  const INITIAL_OPTION_SELECTIONS = new Map();
18
19
  const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
20
+ const IN_STOCK_CODE = 'IN_STOCK';
19
21
 
20
22
  const deriveOptionCodesFromProduct = product => {
21
23
  // If this is a simple product it has no option codes.
@@ -85,6 +87,19 @@ const getIsOutOfStock = (product, optionCodes, optionSelections) => {
85
87
  }
86
88
  return stock_status === OUT_OF_STOCK_CODE;
87
89
  };
90
+ const getIsAllOutOfStock = product => {
91
+ const { stock_status, variants } = product;
92
+ const isConfigurable = isProductConfigurable(product);
93
+
94
+ if (isConfigurable) {
95
+ const inStockItem = variants.find(item => {
96
+ return item.product.stock_status === IN_STOCK_CODE;
97
+ });
98
+ return !inStockItem;
99
+ }
100
+
101
+ return stock_status === OUT_OF_STOCK_CODE;
102
+ };
88
103
 
89
104
  const getMediaGalleryEntries = (product, optionCodes, optionSelections) => {
90
105
  let value = [];
@@ -291,6 +306,8 @@ export const useProductFullDetail = props => {
291
306
  derivedOptionSelections
292
307
  );
293
308
 
309
+ const [singleOptionSelection, setSingleOptionSelection] = useState();
310
+
294
311
  const derivedOptionCodes = useMemo(
295
312
  () => deriveOptionCodesFromProduct(product),
296
313
  [product]
@@ -307,6 +324,41 @@ export const useProductFullDetail = props => {
307
324
  [product, optionCodes, optionSelections]
308
325
  );
309
326
 
327
+ // Check if display out of stock products option is selected in the Admin Dashboard
328
+ const isOutOfStockProductDisplayed = useMemo(() => {
329
+ let totalVariants = 1;
330
+ const isConfigurable = isProductConfigurable(product);
331
+ if (product.configurable_options && isConfigurable) {
332
+ for (const option of product.configurable_options) {
333
+ const length = option.values.length;
334
+ totalVariants = totalVariants * length;
335
+ }
336
+ return product.variants.length === totalVariants;
337
+ }
338
+ }, [product]);
339
+
340
+ const isEverythingOutOfStock = useMemo(() => getIsAllOutOfStock(product), [
341
+ product
342
+ ]);
343
+
344
+ const outOfStockVariants = useMemo(
345
+ () =>
346
+ getOutOfStockVariants(
347
+ product,
348
+ optionCodes,
349
+ singleOptionSelection,
350
+ optionSelections,
351
+ isOutOfStockProductDisplayed
352
+ ),
353
+ [
354
+ product,
355
+ optionCodes,
356
+ singleOptionSelection,
357
+ optionSelections,
358
+ isOutOfStockProductDisplayed
359
+ ]
360
+ );
361
+
310
362
  const mediaGalleryEntries = useMemo(
311
363
  () => getMediaGalleryEntries(product, optionCodes, optionSelections),
312
364
  [product, optionCodes, optionSelections]
@@ -344,7 +396,7 @@ export const useProductFullDetail = props => {
344
396
  optionSelections.forEach((value, key) => {
345
397
  const values = attributeIdToValuesMap.get(key);
346
398
 
347
- const selectedValue = values.find(
399
+ const selectedValue = values?.find(
348
400
  item => item.value_index === value
349
401
  );
350
402
 
@@ -481,6 +533,10 @@ export const useProductFullDetail = props => {
481
533
  const nextOptionSelections = new Map([...optionSelections]);
482
534
  nextOptionSelections.set(optionId, selection);
483
535
  setOptionSelections(nextOptionSelections);
536
+ // Create a new Map to keep track of single selections with key as String
537
+ const nextSingleOptionSelection = new Map();
538
+ nextSingleOptionSelection.set(optionId, selection);
539
+ setSingleOptionSelection(nextSingleOptionSelection);
484
540
  },
485
541
  [optionSelections]
486
542
  );
@@ -542,8 +598,11 @@ export const useProductFullDetail = props => {
542
598
  handleAddToCart,
543
599
  handleSelectionChange,
544
600
  isOutOfStock,
601
+ isEverythingOutOfStock,
602
+ outOfStockVariants,
545
603
  isAddToCartDisabled:
546
604
  isOutOfStock ||
605
+ isEverythingOutOfStock ||
547
606
  isMissingOptions ||
548
607
  isAddConfigurableLoading ||
549
608
  isAddSimpleLoading ||
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from 'react';
2
2
 
3
3
  export const useOptions = props => {
4
- const { onSelectionChange, selectedValues } = props;
4
+ const { onSelectionChange, selectedValues, options } = props;
5
5
  const handleSelectionChange = useCallback(
6
6
  (optionId, selection) => {
7
7
  if (onSelectionChange) {
@@ -12,10 +12,14 @@ export const useOptions = props => {
12
12
  );
13
13
 
14
14
  const selectedValueMap = new Map();
15
- for (const { option_label, value_label } of selectedValues) {
15
+
16
+ // Map the option with correct option_label
17
+ for (const { id, value_label } of selectedValues) {
18
+ const option_label = options.find(
19
+ option => option.attribute_id === String(id)
20
+ ).label;
16
21
  selectedValueMap.set(option_label, value_label);
17
22
  }
18
-
19
23
  return {
20
24
  handleSelectionChange,
21
25
  selectedValueMap
@@ -48,6 +48,21 @@ export const WishlistItemFragment = gql`
48
48
  }
49
49
  }
50
50
  }
51
+ variants {
52
+ attributes {
53
+ uid
54
+ code
55
+ value_index
56
+ }
57
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
58
+ product {
59
+ uid
60
+ stock_status
61
+ small_image {
62
+ url
63
+ }
64
+ }
65
+ }
51
66
  }
52
67
  }
53
68
  # TODO: Use configurable_product_option_uid for ConfigurableWishlistItem when available in 2.4.5
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Rebuild the array of variants with out of stock items data added.
3
+ * Since when admin selects in the Admin dashboard to not to display out of stock products
4
+ * the variants data that are needed to find disabled swatches only show the the in stock ones, missing the out of stock ones
5
+ * We rebuild the variants here to display all the variants and mark the stock status accordingly
6
+ * This returns an array of objects
7
+ */
8
+
9
+ export const createProductVariants = product => {
10
+ const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
11
+ const IN_STOCK_CODE = 'IN_STOCK';
12
+
13
+ if (product && product.configurable_options) {
14
+ const { variants } = product;
15
+ // Compute the permutation of all possible arrays of given arrays
16
+ // For example, if array = [[1,2],[10,20],[100,200,300]]
17
+ // the result is [[1, 10, 100], [1, 10, 200], [1, 10, 300], [1, 20, 100], [1, 20, 200],
18
+ // [1, 20, 300], [2, 10, 100], [2, 10, 200], [2, 10, 300], [2, 20, 100], [2, 20, 200], [2, 20, 300]]
19
+ const cartesian = (...array) =>
20
+ array.reduce((array, current) =>
21
+ array.flatMap(cur => current.map(n => [cur, n].flat()))
22
+ );
23
+
24
+ const configurableOptionsValueIndexes = product.configurable_options.map(
25
+ option => option.values.map(value => value.value_index)
26
+ );
27
+ // Get all possible variants for current options
28
+ const allPossibleItems = cartesian(...configurableOptionsValueIndexes);
29
+
30
+ const variantsValueIndexes = variants.map(variant =>
31
+ variant.attributes.map(attribute => attribute.value_index)
32
+ );
33
+
34
+ const newVariantsArray = [];
35
+ const len = allPossibleItems.length;
36
+ let foundMatch;
37
+ let currentValueIndex = [];
38
+ for (let i = 0; i < len; i++) {
39
+ currentValueIndex = allPossibleItems[i];
40
+ for (const option of variantsValueIndexes) {
41
+ // If found the same item option in the current variants array, meaning the item is in stock
42
+ // If not found a match, meaning the item is out of stock, which is why it's not in the current variants array
43
+ // with the not to display out of stock products selected in Admin dashboard
44
+ foundMatch =
45
+ option.length > 1
46
+ ? Array.from(currentValueIndex)
47
+ .sort()
48
+ .toString() === option.sort().toString()
49
+ : currentValueIndex.toString() === option.toString();
50
+ if (foundMatch) {
51
+ break;
52
+ }
53
+ }
54
+
55
+ const newAttributes = [];
56
+ // If there are more than 1 group of swatches
57
+ if (currentValueIndex.length && currentValueIndex.length > 1) {
58
+ for (const index of Array.from(currentValueIndex)) {
59
+ const code = product.configurable_options.find(option =>
60
+ option.values.find(value => value.value_index === index)
61
+ );
62
+ newAttributes.push({
63
+ value_index: index,
64
+ code: code.attribute_code
65
+ });
66
+ }
67
+ // If there's only one group of swatches
68
+ } else {
69
+ const code = product.configurable_options.find(option =>
70
+ option.values.find(
71
+ value => value.value_index === currentValueIndex
72
+ )
73
+ );
74
+ newAttributes.push({
75
+ value_index: currentValueIndex,
76
+ code: code.attribute_code
77
+ });
78
+ }
79
+ newVariantsArray.push({
80
+ key: i,
81
+ attributes: Array.from(newAttributes),
82
+ product: {
83
+ stock_status: foundMatch ? IN_STOCK_CODE : OUT_OF_STOCK_CODE
84
+ }
85
+ });
86
+ }
87
+ return newVariantsArray;
88
+ } else {
89
+ return [];
90
+ }
91
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Find all the products/variants contains current option selections
3
+ * @return {Array} variants
4
+ */
5
+ export const findAllMatchingVariants = ({
6
+ variants,
7
+ optionCodes,
8
+ singleOptionSelection
9
+ }) => {
10
+ return variants?.filter(({ attributes, product }) => {
11
+ const customAttributes = (attributes || []).reduce(
12
+ (map, { code, value_index }) => new Map(map).set(code, value_index),
13
+ new Map()
14
+ );
15
+ for (const [id, value] of singleOptionSelection) {
16
+ const code = optionCodes.get(id);
17
+
18
+ const matchesStandardAttribute = product[code] === value;
19
+
20
+ const matchesCustomAttribute = customAttributes.get(code) === value;
21
+
22
+ // if any option selection fails to match any standard attribute
23
+ // and also fails to match any custom attribute
24
+ // then this isn't the correct variant
25
+ if (!matchesStandardAttribute && !matchesCustomAttribute) {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ // otherwise, every option selection matched
31
+ // and these are the correct variants
32
+ return true;
33
+ });
34
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Find the combination of k elements in the array.
3
+ * For example: array is [1,2,3]. k=2.
4
+ * The results are [[1,2],[1,3],[2,3]]
5
+ * @return {Array}
6
+ */
7
+ export function getCombinations(array, k, prefix = []) {
8
+ if (k == 0) return [prefix];
9
+ return array.flatMap((value, index) =>
10
+ getCombinations(array.slice(index + 1), k - 1, [...prefix, value])
11
+ );
12
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Find the value_index of out of stock variants
3
+ * @return {Array} indexes
4
+ */
5
+
6
+ export const getOutOfStockIndexes = items => {
7
+ const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
8
+ return items
9
+ ?.filter(item => item.product.stock_status === OUT_OF_STOCK_CODE)
10
+ .map(option =>
11
+ option.attributes.map(attribute => attribute.value_index)
12
+ );
13
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Find out of stock variants/options of current option selections
3
+ * @return {Array} variants
4
+ */
5
+ import { isProductConfigurable } from '@magento/peregrine/lib/util/isProductConfigurable';
6
+ import { findAllMatchingVariants } from '@magento/peregrine/lib/util/findAllMatchingVariants';
7
+ import { getOutOfStockIndexes } from '@magento/peregrine/lib/util/getOutOfStockIndexes';
8
+ import { createProductVariants } from '@magento/peregrine/lib/util/createProductVariants';
9
+ import { getCombinations } from '@magento/peregrine/lib/util/getCombinations';
10
+
11
+ const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
12
+
13
+ export const getOutOfStockVariants = (
14
+ product,
15
+ optionCodes,
16
+ singleOptionSelection,
17
+ optionSelections,
18
+ isOutOfStockProductDisplayed
19
+ ) => {
20
+ const isConfigurable = isProductConfigurable(product);
21
+ const singeOptionSelected =
22
+ singleOptionSelection && singleOptionSelection.size === 1;
23
+ const outOfStockIndexes = [];
24
+
25
+ if (isConfigurable) {
26
+ let variants = product.variants;
27
+ const variantsIfOutOfStockProductsNotDisplayed = createProductVariants(
28
+ product
29
+ );
30
+ //If out of stock products is set to not displayed, use the variants created
31
+ variants = isOutOfStockProductDisplayed
32
+ ? variants
33
+ : variantsIfOutOfStockProductsNotDisplayed;
34
+
35
+ const numberOfVariations = variants[0].attributes.length;
36
+
37
+ // If only one pair of variations, display out of stock variations before option selection
38
+ if (numberOfVariations === 1) {
39
+ const outOfStockOptions = variants.filter(
40
+ variant => variant.product.stock_status === OUT_OF_STOCK_CODE
41
+ );
42
+
43
+ const outOfStockIndex = outOfStockOptions.map(option =>
44
+ option.attributes.map(attribute => attribute.value_index)
45
+ );
46
+ return outOfStockIndex;
47
+ } else {
48
+ if (singeOptionSelected) {
49
+ const optionsSelected =
50
+ Array.from(optionSelections.values()).filter(
51
+ value => !!value
52
+ ).length > 1;
53
+ const selectedIndexes = Array.from(
54
+ optionSelections.values()
55
+ ).flat();
56
+
57
+ const items = findAllMatchingVariants({
58
+ optionCodes,
59
+ singleOptionSelection,
60
+ variants
61
+ });
62
+ const outOfStockItemsIndexes = getOutOfStockIndexes(items);
63
+
64
+ // For all the out of stock options associated with current selection, display out of stock swatches
65
+ // when the number of matching indexes of selected indexes and out of stock indexes are not smaller than the total groups of swatches minus 1
66
+ for (const indexes of outOfStockItemsIndexes) {
67
+ const sameIndexes = indexes.filter(num =>
68
+ selectedIndexes.includes(num)
69
+ );
70
+ const differentIndexes = indexes.filter(
71
+ num => !selectedIndexes.includes(num)
72
+ );
73
+ if (sameIndexes.length >= optionCodes.size - 1) {
74
+ outOfStockIndexes.push(differentIndexes);
75
+ }
76
+ }
77
+ // Display all possible out of stock swatches with current selections, when all groups of swatches are selected
78
+ if (
79
+ optionsSelected &&
80
+ !selectedIndexes.includes(undefined) &&
81
+ selectedIndexes.length === optionCodes.size
82
+ ) {
83
+ const selectedIndexesCombinations = getCombinations(
84
+ selectedIndexes,
85
+ selectedIndexes.length - 1
86
+ );
87
+ // Find out of stock items and indexes for each combination
88
+ const oosIndexes = [];
89
+ for (const option of selectedIndexesCombinations) {
90
+ // Map the option indexes to their optionCodes
91
+ const curOption = new Map(
92
+ [...optionSelections].filter(
93
+ ([key, val]) => (
94
+ option.includes(key), option.includes(val)
95
+ )
96
+ )
97
+ );
98
+ const curItems = findAllMatchingVariants({
99
+ optionCodes: optionCodes,
100
+ singleOptionSelection: curOption,
101
+ variants: variants
102
+ });
103
+ const outOfStockIndex = getOutOfStockIndexes(curItems)
104
+ ?.flat()
105
+ .filter(idx => !selectedIndexes.includes(idx));
106
+ oosIndexes.push(outOfStockIndex);
107
+ }
108
+ return oosIndexes;
109
+ }
110
+ return outOfStockIndexes;
111
+ }
112
+ }
113
+ }
114
+ return [];
115
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Find out of stock variants/options of current option selections with initial selctions
3
+ * @return {Array} variants
4
+ */
5
+ import { findAllMatchingVariants } from '@magento/peregrine/lib/util/findAllMatchingVariants';
6
+ import { getOutOfStockIndexes } from '@magento/peregrine/lib/util/getOutOfStockIndexes';
7
+ import { createProductVariants } from '@magento/peregrine/lib/util/createProductVariants';
8
+ import { getCombinations } from '@magento/peregrine/lib/util/getCombinations';
9
+
10
+ export const getOutOfStockVariantsWithInitialSelection = (
11
+ product,
12
+ configurableOptionCodes,
13
+ multipleOptionSelections,
14
+ configItem,
15
+ isOutOfStockProductDisplayed
16
+ ) => {
17
+ if (configItem && product) {
18
+ let variants = product.variants;
19
+ const variantsIfOutOfStockProductsNotDisplayed = createProductVariants(
20
+ configItem
21
+ );
22
+ //If out of stock products is set to not displayed, use the variants created
23
+ variants = isOutOfStockProductDisplayed
24
+ ? variants
25
+ : variantsIfOutOfStockProductsNotDisplayed;
26
+ if (
27
+ multipleOptionSelections &&
28
+ multipleOptionSelections.size === configurableOptionCodes.size
29
+ ) {
30
+ const selectedIndexes = Array.from(
31
+ multipleOptionSelections.values()
32
+ ).flat();
33
+
34
+ const selectedIndexesCombinations = getCombinations(
35
+ selectedIndexes,
36
+ selectedIndexes.length - 1
37
+ );
38
+ const oosIndexes = [];
39
+ for (const option of selectedIndexesCombinations) {
40
+ const curOption = new Map(
41
+ [...multipleOptionSelections].filter(
42
+ ([key, val]) => (
43
+ option.includes(key), option.includes(val)
44
+ )
45
+ )
46
+ );
47
+ const curItems = findAllMatchingVariants({
48
+ optionCodes: configurableOptionCodes,
49
+ singleOptionSelection: curOption,
50
+ variants: variants
51
+ });
52
+
53
+ const outOfStockIndex = getOutOfStockIndexes(curItems)
54
+ ?.flat()
55
+ .filter(idx => !selectedIndexes.includes(idx));
56
+
57
+ oosIndexes.push(outOfStockIndex);
58
+ }
59
+ return oosIndexes;
60
+ }
61
+ return [];
62
+ }
63
+ };
@@ -1,3 +1,3 @@
1
1
  // TODO: Move/merge with product util in peregrine?
2
2
  export const isProductConfigurable = product =>
3
- product.__typename === 'ConfigurableProduct';
3
+ product?.__typename === 'ConfigurableProduct';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magento/peregrine",
3
- "version": "12.5.1-beta.1",
3
+ "version": "12.6.0-alpha.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },