@riosst100/pwa-marketplace 1.8.1 → 1.8.2

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 (60) hide show
  1. package/i18n/id_ID.json +508 -508
  2. package/package.json +1 -2
  3. package/src/components/CustomSubCategory/subCategory.js +0 -14
  4. package/src/components/FilterTop/FilterBlockList/filterTopItemGroup.module.css +1 -1
  5. package/src/components/FilterTop/filterTop.js +2 -2
  6. package/src/components/FilterTop/filterTop.module.css +1 -1
  7. package/src/components/FilterTopBackup/CustomFilters/customFilter.js +83 -83
  8. package/src/components/FilterTopBackup/CustomFilters/customFilter.module.css +21 -21
  9. package/src/components/FilterTopBackup/CustomFilters/customFilters.js +131 -131
  10. package/src/components/FilterTopBackup/CustomFilters/customFilters.module.css +22 -22
  11. package/src/components/FilterTopBackup/CustomFilters/index.js +1 -1
  12. package/src/components/FilterTopBackup/filterTop.js +14 -14
  13. package/src/components/FilterTopBackup/filterTop.module.css +22 -22
  14. package/src/components/FilterTopBackup/filterTop.shimmer.js +24 -24
  15. package/src/components/FilterTopBackup/index.js +2 -2
  16. package/src/components/Header/websiteSwitcher.shimmer.js +6 -6
  17. package/src/components/Header/websiteSwitcherItem.js +47 -47
  18. package/src/components/Header/websiteSwitcherItem.module.css +20 -20
  19. package/src/components/PhoneTextInput/index.js +1 -1
  20. package/src/components/PhoneTextInput/phoneTextInput.js +62 -62
  21. package/src/components/ProductListTab/index.js +4 -4
  22. package/src/components/ProductListTab/productListTab.module.css +64 -64
  23. package/src/components/ProductListTab/productListTab.shimmer.js +24 -24
  24. package/src/components/SellerCountry/index.js +1 -1
  25. package/src/components/SellerCountry/sellerCountry.js +71 -71
  26. package/src/components/SellerCountry/sellerCountry.module.css +3 -3
  27. package/src/components/ShopByCategory/index.js +2 -2
  28. package/src/components/ShopByCategory/shopByCategory.js +69 -69
  29. package/src/components/ShopByCategory/shopByCategory.module.css +58 -58
  30. package/src/components/ShopByCategory/shopByCategory.shimmer.js +24 -24
  31. package/src/components/SubCategory/subCategory.js +1 -1
  32. package/src/components/WebsiteSwitcher/websiteSwitcher.shimmer.js +6 -6
  33. package/src/components/WebsiteSwitcher/websiteSwitcherItem.js +47 -47
  34. package/src/components/WebsiteSwitcher/websiteSwitcherItem.module.css +20 -20
  35. package/src/overwrites/peregrine/lib/talons/MegaMenu/megaMenu.gql.js +96 -96
  36. package/src/overwrites/peregrine/lib/talons/MegaMenu/useMegaMenu.js +199 -199
  37. package/src/overwrites/peregrine/lib/talons/MegaMenu/useMegaMenuItem.js +66 -66
  38. package/src/overwrites/peregrine/lib/talons/MegaMenu/useSubMenu.js +20 -20
  39. package/src/overwrites/peregrine/lib/talons/ProductFullDetail/useProductFullDetail.js +642 -642
  40. package/src/overwrites/peregrine/lib/talons/RootComponents/Category/category.gql.js +49 -49
  41. package/src/overwrites/peregrine/lib/talons/RootComponents/Category/useCategory.js +227 -227
  42. package/src/overwrites/peregrine/lib/talons/RootComponents/Product/product.gql.js +31 -31
  43. package/src/overwrites/peregrine/lib/talons/RootComponents/Product/productDetailFragment.gql.js +235 -235
  44. package/src/overwrites/venia-ui/lib/components/Header/cartTrigger.module.css +47 -47
  45. package/src/overwrites/venia-ui/lib/components/RadioGroup/radio.js +60 -60
  46. package/src/overwrites/venia-ui/lib/components/RadioGroup/radio.module.css +70 -70
  47. package/src/talons/AttributesBlock/attributesBlock.gql.js +15 -15
  48. package/src/talons/CustomFilters/customFilters.gql.js +45 -45
  49. package/src/talons/CustomFilters/useCustomFilters.js +5 -2
  50. package/src/talons/FilterTop/filterTop.gql.js +45 -45
  51. package/src/talons/FilterTop/index.js +1 -1
  52. package/src/talons/FilterTop/useFilterTop.js +5 -4
  53. package/src/talons/Header/websiteSwitcher.gql.js +45 -45
  54. package/src/talons/SellerReview/sellerReview.gql.js +53 -53
  55. package/src/talons/ShopByCategory/index.js +1 -1
  56. package/src/talons/ShopByCategory/shopByCategory.gql.js +38 -38
  57. package/src/talons/ShopByCategory/useShopByCategory.js +69 -69
  58. package/src/talons/SubCategory/subCategory.gql.js +15 -15
  59. package/src/talons/SubCategory/useSubCategory.js +3 -3
  60. package/src/talons/WebsiteSwitcher/websiteSwitcher.gql.js +45 -45
@@ -1,642 +1,642 @@
1
- import { useCallback, useState, useMemo } from 'react';
2
- import { useIntl } from 'react-intl';
3
- import { useMutation, useQuery } from '@apollo/client';
4
- import { useCartContext } from '@magento/peregrine/lib/context/cart';
5
- import { useUserContext } from '@magento/peregrine/lib/context/user';
6
-
7
- import { appendOptionsToPayload } from '@magento/peregrine/lib/util/appendOptionsToPayload';
8
- import { findMatchingVariant } from '@magento/peregrine/lib/util/findMatchingProductVariant';
9
- import { isProductConfigurable } from '@magento/peregrine/lib/util/isProductConfigurable';
10
- import { isSupportedProductType as isSupported } from '@magento/peregrine/lib/util/isSupportedProductType';
11
- import { deriveErrorMessage } from '@magento/peregrine/lib/util/deriveErrorMessage';
12
- import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
13
- import defaultOperations from '@magento/peregrine/lib/talons/ProductFullDetail/productFullDetail.gql';
14
- import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
15
- import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
16
-
17
- const INITIAL_OPTION_CODES = new Map();
18
- const INITIAL_OPTION_SELECTIONS = new Map();
19
- const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
20
- const IN_STOCK_CODE = 'IN_STOCK';
21
-
22
- const deriveOptionCodesFromProduct = product => {
23
- // If this is a simple product it has no option codes.
24
- if (!isProductConfigurable(product)) {
25
- return INITIAL_OPTION_CODES;
26
- }
27
-
28
- // Initialize optionCodes based on the options of the product.
29
- const initialOptionCodes = new Map();
30
- for (const {
31
- attribute_id,
32
- attribute_code
33
- } of product.configurable_options) {
34
- initialOptionCodes.set(attribute_id, attribute_code);
35
- }
36
-
37
- return initialOptionCodes;
38
- };
39
-
40
- // Similar to deriving the initial codes for each option.
41
- const deriveOptionSelectionsFromProduct = product => {
42
- if (!isProductConfigurable(product)) {
43
- return INITIAL_OPTION_SELECTIONS;
44
- }
45
-
46
- const initialOptionSelections = new Map();
47
- for (const { attribute_id } of product.configurable_options) {
48
- initialOptionSelections.set(attribute_id, undefined);
49
- }
50
-
51
- return initialOptionSelections;
52
- };
53
-
54
- const getIsMissingOptions = (product, optionSelections) => {
55
- // Non-configurable products can't be missing options.
56
- if (!isProductConfigurable(product)) {
57
- return false;
58
- }
59
-
60
- // Configurable products are missing options if we have fewer
61
- // option selections than the product has options.
62
- const { configurable_options } = product;
63
- const numProductOptions = configurable_options.length;
64
- const numProductSelections = Array.from(optionSelections.values()).filter(
65
- value => !!value
66
- ).length;
67
-
68
- return numProductSelections < numProductOptions;
69
- };
70
-
71
- const getIsOutOfStock = (product, optionCodes, optionSelections) => {
72
- const { stock_status, variants } = product;
73
- const isConfigurable = isProductConfigurable(product);
74
- const optionsSelected =
75
- Array.from(optionSelections.values()).filter(value => !!value).length >
76
- 0;
77
-
78
- if (isConfigurable && optionsSelected) {
79
- const item = findMatchingVariant({
80
- optionCodes,
81
- optionSelections,
82
- variants
83
- });
84
- const stockStatus = item?.product?.stock_status;
85
-
86
- return stockStatus === OUT_OF_STOCK_CODE || !stockStatus;
87
- }
88
- return stock_status === OUT_OF_STOCK_CODE;
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
- };
103
-
104
- const getMediaGalleryEntries = (product, optionCodes, optionSelections) => {
105
- let value = [];
106
-
107
- const { media_gallery_entries, variants } = product;
108
- const isConfigurable = isProductConfigurable(product);
109
-
110
- // Selections are initialized to "code => undefined". Once we select a value, like color, the selections change. This filters out unselected options.
111
- const optionsSelected =
112
- Array.from(optionSelections.values()).filter(value => !!value).length >
113
- 0;
114
-
115
- if (!isConfigurable || !optionsSelected) {
116
- value = media_gallery_entries;
117
- } else {
118
- // If any of the possible variants matches the selection add that
119
- // variant's image to the media gallery. NOTE: This _can_, and does,
120
- // include variants such as size. If Magento is configured to display
121
- // an image for a size attribute, it will render that image.
122
- const item = findMatchingVariant({
123
- optionCodes,
124
- optionSelections,
125
- variants
126
- });
127
-
128
- value = item
129
- ? [...item.product.media_gallery_entries, ...media_gallery_entries]
130
- : media_gallery_entries;
131
- }
132
-
133
- return value;
134
- };
135
-
136
- // We only want to display breadcrumbs for one category on a PDP even if a
137
- // product has multiple related categories. This function filters and selects
138
- // one category id for that purpose.
139
- const getBreadcrumbCategoryId = categories => {
140
- // Exit if there are no categories for this product.
141
- if (!categories || !categories.length) {
142
- return;
143
- }
144
- const breadcrumbSet = new Set();
145
- categories.forEach(({ breadcrumbs }) => {
146
- // breadcrumbs can be `null`...
147
- (breadcrumbs || []).forEach(({ category_id }) =>
148
- breadcrumbSet.add(category_id)
149
- );
150
- });
151
-
152
- // Until we can get the single canonical breadcrumb path to a product we
153
- // will just return the first category id of the potential leaf categories.
154
- const leafCategory = categories.find(
155
- category => !breadcrumbSet.has(category.uid)
156
- );
157
-
158
- // If we couldn't find a leaf category then just use the first category
159
- // in the list for this product.
160
- return leafCategory.uid || categories[0].uid;
161
- };
162
-
163
- const getConfigPrice = (product, optionCodes, optionSelections) => {
164
- let value;
165
-
166
- const { variants } = product;
167
- const isConfigurable = isProductConfigurable(product);
168
-
169
- const optionsSelected =
170
- Array.from(optionSelections.values()).filter(value => !!value).length >
171
- 0;
172
-
173
- if (!isConfigurable || !optionsSelected) {
174
- value = product.price_range?.maximum_price;
175
- } else {
176
- const item = findMatchingVariant({
177
- optionCodes,
178
- optionSelections,
179
- variants
180
- });
181
-
182
- value = item
183
- ? item.product.price_range?.maximum_price
184
- : product.price_range?.maximum_price;
185
- }
186
-
187
- return value;
188
- };
189
-
190
- const attributeLabelCompare = (attribute1, attribute2) => {
191
- const label1 = attribute1['attribute_metadata']['label'].toLowerCase();
192
- const label2 = attribute2['attribute_metadata']['label'].toLowerCase();
193
- if (label1 < label2) return -1;
194
- else if (label1 > label2) return 1;
195
- else return 0;
196
- };
197
-
198
- const getCustomAttributes = (product, optionCodes, optionSelections) => {
199
- const { custom_attributes, variants } = product;
200
- const isConfigurable = isProductConfigurable(product);
201
- const optionsSelected =
202
- Array.from(optionSelections.values()).filter(value => !!value).length >
203
- 0;
204
-
205
- if (isConfigurable && optionsSelected) {
206
- const item = findMatchingVariant({
207
- optionCodes,
208
- optionSelections,
209
- variants
210
- });
211
-
212
- return item && item.product
213
- ? [...item.product.custom_attributes].sort(attributeLabelCompare)
214
- : [];
215
- }
216
-
217
- return custom_attributes
218
- ? [...custom_attributes].sort(attributeLabelCompare)
219
- : [];
220
- };
221
-
222
- /**
223
- * @param {GraphQLDocument} props.addConfigurableProductToCartMutation - configurable product mutation
224
- * @param {GraphQLDocument} props.addSimpleProductToCartMutation - configurable product mutation
225
- * @param {Object.<string, GraphQLDocument>} props.operations - collection of operation overrides merged into defaults
226
- * @param {Object} props.product - the product, see RootComponents/Product
227
- *
228
- * @returns {{
229
- * breadcrumbCategoryId: string|undefined,
230
- * errorMessage: string|undefined,
231
- * handleAddToCart: func,
232
- * handleSelectionChange: func,
233
- * handleSetQuantity: func,
234
- * isAddToCartDisabled: boolean,
235
- * isSupportedProductType: boolean,
236
- * mediaGalleryEntries: array,
237
- * productDetails: object,
238
- * quantity: number
239
- * }}
240
- */
241
- export const useProductFullDetail = props => {
242
- const {
243
- addConfigurableProductToCartMutation,
244
- addSimpleProductToCartMutation,
245
- product
246
- } = props;
247
-
248
- const [, { dispatch }] = useEventingContext();
249
-
250
- const hasDeprecatedOperationProp = !!(
251
- addConfigurableProductToCartMutation || addSimpleProductToCartMutation
252
- );
253
-
254
- const operations = mergeOperations(defaultOperations, props.operations);
255
-
256
- const productType = product.__typename;
257
-
258
- const isSupportedProductType = isSupported(productType);
259
-
260
- const [{ cartId }] = useCartContext();
261
- const [{ isSignedIn }] = useUserContext();
262
- const { formatMessage } = useIntl();
263
-
264
- const { data: storeConfigData } = useQuery(
265
- operations.getWishlistConfigQuery,
266
- {
267
- fetchPolicy: 'cache-and-network'
268
- }
269
- );
270
-
271
- const [
272
- addConfigurableProductToCart,
273
- {
274
- error: errorAddingConfigurableProduct,
275
- loading: isAddConfigurableLoading
276
- }
277
- ] = useMutation(
278
- addConfigurableProductToCartMutation ||
279
- operations.addConfigurableProductToCartMutation
280
- );
281
-
282
- const [
283
- addSimpleProductToCart,
284
- { error: errorAddingSimpleProduct, loading: isAddSimpleLoading }
285
- ] = useMutation(
286
- addSimpleProductToCartMutation ||
287
- operations.addSimpleProductToCartMutation
288
- );
289
-
290
- const [
291
- addProductToCart,
292
- {
293
- data: addToCartResponseData,
294
- error: errorAddingProductToCart,
295
- loading: isAddProductLoading
296
- }
297
- ] = useMutation(operations.addProductToCartMutation);
298
-
299
- const breadcrumbCategoryId = useMemo(
300
- () => getBreadcrumbCategoryId(product.categories),
301
- [product.categories]
302
- );
303
-
304
- const derivedOptionSelections = useMemo(
305
- () => deriveOptionSelectionsFromProduct(product),
306
- [product]
307
- );
308
-
309
- const [optionSelections, setOptionSelections] = useState(
310
- derivedOptionSelections
311
- );
312
-
313
- const [singleOptionSelection, setSingleOptionSelection] = useState();
314
-
315
- const derivedOptionCodes = useMemo(
316
- () => deriveOptionCodesFromProduct(product),
317
- [product]
318
- );
319
- const [optionCodes] = useState(derivedOptionCodes);
320
-
321
- const isMissingOptions = useMemo(
322
- () => getIsMissingOptions(product, optionSelections),
323
- [product, optionSelections]
324
- );
325
-
326
- const isOutOfStock = useMemo(
327
- () => getIsOutOfStock(product, optionCodes, optionSelections),
328
- [product, optionCodes, optionSelections]
329
- );
330
-
331
- // Check if display out of stock products option is selected in the Admin Dashboard
332
- const isOutOfStockProductDisplayed = useMemo(() => {
333
- let totalVariants = 1;
334
- const isConfigurable = isProductConfigurable(product);
335
- if (product.configurable_options && isConfigurable) {
336
- for (const option of product.configurable_options) {
337
- const length = option.values.length;
338
- totalVariants = totalVariants * length;
339
- }
340
- return product.variants.length === totalVariants;
341
- }
342
- }, [product]);
343
-
344
- const isEverythingOutOfStock = useMemo(() => getIsAllOutOfStock(product), [
345
- product
346
- ]);
347
-
348
- const outOfStockVariants = useMemo(
349
- () =>
350
- getOutOfStockVariants(
351
- product,
352
- optionCodes,
353
- singleOptionSelection,
354
- optionSelections,
355
- isOutOfStockProductDisplayed
356
- ),
357
- [
358
- product,
359
- optionCodes,
360
- singleOptionSelection,
361
- optionSelections,
362
- isOutOfStockProductDisplayed
363
- ]
364
- );
365
-
366
- const mediaGalleryEntries = useMemo(
367
- () => getMediaGalleryEntries(product, optionCodes, optionSelections),
368
- [product, optionCodes, optionSelections]
369
- );
370
-
371
- const customAttributes = useMemo(
372
- () => getCustomAttributes(product, optionCodes, optionSelections),
373
- [product, optionCodes, optionSelections]
374
- );
375
-
376
- const productPrice = useMemo(
377
- () => getConfigPrice(product, optionCodes, optionSelections),
378
- [product, optionCodes, optionSelections]
379
- );
380
-
381
- // The map of ids to values (and their uids)
382
- // For example:
383
- // { "179" => [{ uid: "abc", value_index: 1 }, { uid: "def", value_index: 2 }]}
384
- const attributeIdToValuesMap = useMemo(() => {
385
- const map = new Map();
386
- // For simple items, this will be an empty map.
387
- const options = product.configurable_options || [];
388
- for (const { attribute_id, values } of options) {
389
- map.set(attribute_id, values);
390
- }
391
- return map;
392
- }, [product.configurable_options]);
393
-
394
- // An array of selected option uids. Useful for passing to mutations.
395
- // For example:
396
- // ["abc", "def"]
397
- const selectedOptionsArray = useMemo(() => {
398
- const selectedOptions = [];
399
-
400
- optionSelections.forEach((value, key) => {
401
- const values = attributeIdToValuesMap.get(key);
402
-
403
- const selectedValue = values?.find(
404
- item => item.value_index === value
405
- );
406
-
407
- if (selectedValue) {
408
- selectedOptions.push(selectedValue.uid);
409
- }
410
- });
411
- return selectedOptions;
412
- }, [attributeIdToValuesMap, optionSelections]);
413
-
414
- const handleAddToCart = useCallback(
415
- async formValues => {
416
- const { quantity } = formValues;
417
-
418
- /*
419
- @deprecated in favor of general addProductsToCart mutation. Will support until the next MAJOR.
420
- */
421
- if (hasDeprecatedOperationProp) {
422
- const payload = {
423
- item: product,
424
- productType,
425
- quantity
426
- };
427
-
428
- if (isProductConfigurable(product)) {
429
- appendOptionsToPayload(
430
- payload,
431
- optionSelections,
432
- optionCodes
433
- );
434
- }
435
-
436
- if (isSupportedProductType) {
437
- const variables = {
438
- cartId,
439
- parentSku: payload.parentSku,
440
- product: payload.item,
441
- quantity: payload.quantity,
442
- sku: payload.item.sku
443
- };
444
- // Use the proper mutation for the type.
445
- if (productType === 'SimpleProduct') {
446
- try {
447
- await addSimpleProductToCart({
448
- variables
449
- });
450
- } catch {
451
- return;
452
- }
453
- } else if (productType === 'ConfigurableProduct') {
454
- try {
455
- await addConfigurableProductToCart({
456
- variables
457
- });
458
- } catch {
459
- return;
460
- }
461
- }
462
- } else {
463
- console.error(
464
- 'Unsupported product type. Cannot add to cart.'
465
- );
466
- }
467
- } else {
468
- const variables = {
469
- cartId,
470
- product: {
471
- sku: product.sku,
472
- quantity
473
- },
474
- entered_options: [
475
- {
476
- uid: product.uid,
477
- value: product.name
478
- }
479
- ]
480
- };
481
-
482
- if (selectedOptionsArray.length) {
483
- variables.product.selected_options = selectedOptionsArray;
484
- }
485
-
486
- try {
487
- await addProductToCart({ variables });
488
-
489
- const selectedOptionsLabels =
490
- selectedOptionsArray?.map((uid, i) => ({
491
- attribute: product.configurable_options[i].label,
492
- value:
493
- product.configurable_options[i].values.findLast(
494
- x => x.uid === uid
495
- )?.label || null
496
- })) || null;
497
-
498
- dispatch({
499
- type: 'CART_ADD_ITEM',
500
- payload: {
501
- cartId,
502
- sku: product.sku,
503
- name: product.name,
504
- pricing: product.price,
505
- priceTotal: productPrice.final_price.value,
506
- currencyCode: productPrice.final_price.currency,
507
- discountAmount: productPrice.discount.amount_off,
508
- selectedOptions: selectedOptionsLabels,
509
- quantity
510
- }
511
- });
512
- } catch {
513
- return;
514
- }
515
- }
516
- },
517
- [
518
- addConfigurableProductToCart,
519
- addProductToCart,
520
- addSimpleProductToCart,
521
- cartId,
522
- dispatch,
523
- hasDeprecatedOperationProp,
524
- isSupportedProductType,
525
- optionCodes,
526
- optionSelections,
527
- product,
528
- productPrice,
529
- productType,
530
- selectedOptionsArray
531
- ]
532
- );
533
-
534
- const handleSelectionChange = useCallback(
535
- (optionId, selection) => {
536
- // We must create a new Map here so that React knows that the value
537
- // of optionSelections has changed.
538
- const nextOptionSelections = new Map([...optionSelections]);
539
- nextOptionSelections.set(optionId, selection);
540
- setOptionSelections(nextOptionSelections);
541
- // Create a new Map to keep track of single selections with key as String
542
- const nextSingleOptionSelection = new Map();
543
- nextSingleOptionSelection.set(optionId, selection);
544
- setSingleOptionSelection(nextSingleOptionSelection);
545
- },
546
- [optionSelections]
547
- );
548
-
549
- // Normalization object for product details we need for rendering.
550
- const productDetails = {
551
- description: product.description,
552
- shortDescription: product.short_description,
553
- name: product.name,
554
- price: productPrice?.final_price,
555
- price_range: product?.price_range,
556
- sku: product.sku,
557
- term_and_conditions: product.term_and_conditions,
558
- shipping_policy: product.shipping_policy,
559
- return_policy: product.return_policy,
560
- preorder: product.preorder,
561
- auction_data: product.auction_data
562
- };
563
-
564
- const sellerDetails = {
565
- name: product.seller?.name,
566
- url_key: product.seller?.url_key,
567
- city: product.seller?.city,
568
- country: product.seller?.country
569
- };
570
-
571
- const derivedErrorMessage = useMemo(
572
- () =>
573
- deriveErrorMessage([
574
- errorAddingSimpleProduct,
575
- errorAddingConfigurableProduct,
576
- errorAddingProductToCart,
577
- ...(addToCartResponseData?.addProductsToCart?.user_errors || [])
578
- ]),
579
- [
580
- errorAddingConfigurableProduct,
581
- errorAddingProductToCart,
582
- errorAddingSimpleProduct,
583
- addToCartResponseData
584
- ]
585
- );
586
-
587
- const wishlistItemOptions = useMemo(() => {
588
- const options = {
589
- quantity: 1,
590
- sku: product.sku
591
- };
592
-
593
- if (productType === 'ConfigurableProduct') {
594
- options.selected_options = selectedOptionsArray;
595
- }
596
-
597
- return options;
598
- }, [product, productType, selectedOptionsArray]);
599
-
600
- const wishlistButtonProps = {
601
- buttonText: isSelected =>
602
- isSelected
603
- ? formatMessage({
604
- id: 'wishlistButton.addedText',
605
- defaultMessage: 'Added to Favorites'
606
- })
607
- : formatMessage({
608
- id: 'wishlistButton.addText',
609
- defaultMessage: 'Add to Favorites'
610
- }),
611
- item: wishlistItemOptions,
612
- storeConfig: storeConfigData ? storeConfigData.storeConfig : {}
613
- };
614
-
615
- return {
616
- breadcrumbCategoryId,
617
- errorMessage: derivedErrorMessage,
618
- handleAddToCart,
619
- handleSelectionChange,
620
- isOutOfStock,
621
- isEverythingOutOfStock,
622
- outOfStockVariants,
623
- isAddToCartDisabled:
624
- isOutOfStock ||
625
- isEverythingOutOfStock ||
626
- isMissingOptions ||
627
- isAddConfigurableLoading ||
628
- isAddSimpleLoading ||
629
- isAddProductLoading,
630
- isSupportedProductType,
631
- mediaGalleryEntries,
632
- shouldShowWishlistButton:
633
- isSignedIn &&
634
- storeConfigData &&
635
- !!storeConfigData.storeConfig.magento_wishlist_general_is_enabled,
636
- productDetails,
637
- customAttributes,
638
- wishlistButtonProps,
639
- wishlistItemOptions,
640
- sellerDetails
641
- };
642
- };
1
+ import { useCallback, useState, useMemo } from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ import { useMutation, useQuery } from '@apollo/client';
4
+ import { useCartContext } from '@magento/peregrine/lib/context/cart';
5
+ import { useUserContext } from '@magento/peregrine/lib/context/user';
6
+
7
+ import { appendOptionsToPayload } from '@magento/peregrine/lib/util/appendOptionsToPayload';
8
+ import { findMatchingVariant } from '@magento/peregrine/lib/util/findMatchingProductVariant';
9
+ import { isProductConfigurable } from '@magento/peregrine/lib/util/isProductConfigurable';
10
+ import { isSupportedProductType as isSupported } from '@magento/peregrine/lib/util/isSupportedProductType';
11
+ import { deriveErrorMessage } from '@magento/peregrine/lib/util/deriveErrorMessage';
12
+ import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
13
+ import defaultOperations from '@magento/peregrine/lib/talons/ProductFullDetail/productFullDetail.gql';
14
+ import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
15
+ import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
16
+
17
+ const INITIAL_OPTION_CODES = new Map();
18
+ const INITIAL_OPTION_SELECTIONS = new Map();
19
+ const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
20
+ const IN_STOCK_CODE = 'IN_STOCK';
21
+
22
+ const deriveOptionCodesFromProduct = product => {
23
+ // If this is a simple product it has no option codes.
24
+ if (!isProductConfigurable(product)) {
25
+ return INITIAL_OPTION_CODES;
26
+ }
27
+
28
+ // Initialize optionCodes based on the options of the product.
29
+ const initialOptionCodes = new Map();
30
+ for (const {
31
+ attribute_id,
32
+ attribute_code
33
+ } of product.configurable_options) {
34
+ initialOptionCodes.set(attribute_id, attribute_code);
35
+ }
36
+
37
+ return initialOptionCodes;
38
+ };
39
+
40
+ // Similar to deriving the initial codes for each option.
41
+ const deriveOptionSelectionsFromProduct = product => {
42
+ if (!isProductConfigurable(product)) {
43
+ return INITIAL_OPTION_SELECTIONS;
44
+ }
45
+
46
+ const initialOptionSelections = new Map();
47
+ for (const { attribute_id } of product.configurable_options) {
48
+ initialOptionSelections.set(attribute_id, undefined);
49
+ }
50
+
51
+ return initialOptionSelections;
52
+ };
53
+
54
+ const getIsMissingOptions = (product, optionSelections) => {
55
+ // Non-configurable products can't be missing options.
56
+ if (!isProductConfigurable(product)) {
57
+ return false;
58
+ }
59
+
60
+ // Configurable products are missing options if we have fewer
61
+ // option selections than the product has options.
62
+ const { configurable_options } = product;
63
+ const numProductOptions = configurable_options.length;
64
+ const numProductSelections = Array.from(optionSelections.values()).filter(
65
+ value => !!value
66
+ ).length;
67
+
68
+ return numProductSelections < numProductOptions;
69
+ };
70
+
71
+ const getIsOutOfStock = (product, optionCodes, optionSelections) => {
72
+ const { stock_status, variants } = product;
73
+ const isConfigurable = isProductConfigurable(product);
74
+ const optionsSelected =
75
+ Array.from(optionSelections.values()).filter(value => !!value).length >
76
+ 0;
77
+
78
+ if (isConfigurable && optionsSelected) {
79
+ const item = findMatchingVariant({
80
+ optionCodes,
81
+ optionSelections,
82
+ variants
83
+ });
84
+ const stockStatus = item?.product?.stock_status;
85
+
86
+ return stockStatus === OUT_OF_STOCK_CODE || !stockStatus;
87
+ }
88
+ return stock_status === OUT_OF_STOCK_CODE;
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
+ };
103
+
104
+ const getMediaGalleryEntries = (product, optionCodes, optionSelections) => {
105
+ let value = [];
106
+
107
+ const { media_gallery_entries, variants } = product;
108
+ const isConfigurable = isProductConfigurable(product);
109
+
110
+ // Selections are initialized to "code => undefined". Once we select a value, like color, the selections change. This filters out unselected options.
111
+ const optionsSelected =
112
+ Array.from(optionSelections.values()).filter(value => !!value).length >
113
+ 0;
114
+
115
+ if (!isConfigurable || !optionsSelected) {
116
+ value = media_gallery_entries;
117
+ } else {
118
+ // If any of the possible variants matches the selection add that
119
+ // variant's image to the media gallery. NOTE: This _can_, and does,
120
+ // include variants such as size. If Magento is configured to display
121
+ // an image for a size attribute, it will render that image.
122
+ const item = findMatchingVariant({
123
+ optionCodes,
124
+ optionSelections,
125
+ variants
126
+ });
127
+
128
+ value = item
129
+ ? [...item.product.media_gallery_entries, ...media_gallery_entries]
130
+ : media_gallery_entries;
131
+ }
132
+
133
+ return value;
134
+ };
135
+
136
+ // We only want to display breadcrumbs for one category on a PDP even if a
137
+ // product has multiple related categories. This function filters and selects
138
+ // one category id for that purpose.
139
+ const getBreadcrumbCategoryId = categories => {
140
+ // Exit if there are no categories for this product.
141
+ if (!categories || !categories.length) {
142
+ return;
143
+ }
144
+ const breadcrumbSet = new Set();
145
+ categories.forEach(({ breadcrumbs }) => {
146
+ // breadcrumbs can be `null`...
147
+ (breadcrumbs || []).forEach(({ category_id }) =>
148
+ breadcrumbSet.add(category_id)
149
+ );
150
+ });
151
+
152
+ // Until we can get the single canonical breadcrumb path to a product we
153
+ // will just return the first category id of the potential leaf categories.
154
+ const leafCategory = categories.find(
155
+ category => !breadcrumbSet.has(category.uid)
156
+ );
157
+
158
+ // If we couldn't find a leaf category then just use the first category
159
+ // in the list for this product.
160
+ return leafCategory.uid || categories[0].uid;
161
+ };
162
+
163
+ const getConfigPrice = (product, optionCodes, optionSelections) => {
164
+ let value;
165
+
166
+ const { variants } = product;
167
+ const isConfigurable = isProductConfigurable(product);
168
+
169
+ const optionsSelected =
170
+ Array.from(optionSelections.values()).filter(value => !!value).length >
171
+ 0;
172
+
173
+ if (!isConfigurable || !optionsSelected) {
174
+ value = product.price_range?.maximum_price;
175
+ } else {
176
+ const item = findMatchingVariant({
177
+ optionCodes,
178
+ optionSelections,
179
+ variants
180
+ });
181
+
182
+ value = item
183
+ ? item.product.price_range?.maximum_price
184
+ : product.price_range?.maximum_price;
185
+ }
186
+
187
+ return value;
188
+ };
189
+
190
+ const attributeLabelCompare = (attribute1, attribute2) => {
191
+ const label1 = attribute1['attribute_metadata']['label'].toLowerCase();
192
+ const label2 = attribute2['attribute_metadata']['label'].toLowerCase();
193
+ if (label1 < label2) return -1;
194
+ else if (label1 > label2) return 1;
195
+ else return 0;
196
+ };
197
+
198
+ const getCustomAttributes = (product, optionCodes, optionSelections) => {
199
+ const { custom_attributes, variants } = product;
200
+ const isConfigurable = isProductConfigurable(product);
201
+ const optionsSelected =
202
+ Array.from(optionSelections.values()).filter(value => !!value).length >
203
+ 0;
204
+
205
+ if (isConfigurable && optionsSelected) {
206
+ const item = findMatchingVariant({
207
+ optionCodes,
208
+ optionSelections,
209
+ variants
210
+ });
211
+
212
+ return item && item.product
213
+ ? [...item.product.custom_attributes].sort(attributeLabelCompare)
214
+ : [];
215
+ }
216
+
217
+ return custom_attributes
218
+ ? [...custom_attributes].sort(attributeLabelCompare)
219
+ : [];
220
+ };
221
+
222
+ /**
223
+ * @param {GraphQLDocument} props.addConfigurableProductToCartMutation - configurable product mutation
224
+ * @param {GraphQLDocument} props.addSimpleProductToCartMutation - configurable product mutation
225
+ * @param {Object.<string, GraphQLDocument>} props.operations - collection of operation overrides merged into defaults
226
+ * @param {Object} props.product - the product, see RootComponents/Product
227
+ *
228
+ * @returns {{
229
+ * breadcrumbCategoryId: string|undefined,
230
+ * errorMessage: string|undefined,
231
+ * handleAddToCart: func,
232
+ * handleSelectionChange: func,
233
+ * handleSetQuantity: func,
234
+ * isAddToCartDisabled: boolean,
235
+ * isSupportedProductType: boolean,
236
+ * mediaGalleryEntries: array,
237
+ * productDetails: object,
238
+ * quantity: number
239
+ * }}
240
+ */
241
+ export const useProductFullDetail = props => {
242
+ const {
243
+ addConfigurableProductToCartMutation,
244
+ addSimpleProductToCartMutation,
245
+ product
246
+ } = props;
247
+
248
+ const [, { dispatch }] = useEventingContext();
249
+
250
+ const hasDeprecatedOperationProp = !!(
251
+ addConfigurableProductToCartMutation || addSimpleProductToCartMutation
252
+ );
253
+
254
+ const operations = mergeOperations(defaultOperations, props.operations);
255
+
256
+ const productType = product.__typename;
257
+
258
+ const isSupportedProductType = isSupported(productType);
259
+
260
+ const [{ cartId }] = useCartContext();
261
+ const [{ isSignedIn }] = useUserContext();
262
+ const { formatMessage } = useIntl();
263
+
264
+ const { data: storeConfigData } = useQuery(
265
+ operations.getWishlistConfigQuery,
266
+ {
267
+ fetchPolicy: 'cache-and-network'
268
+ }
269
+ );
270
+
271
+ const [
272
+ addConfigurableProductToCart,
273
+ {
274
+ error: errorAddingConfigurableProduct,
275
+ loading: isAddConfigurableLoading
276
+ }
277
+ ] = useMutation(
278
+ addConfigurableProductToCartMutation ||
279
+ operations.addConfigurableProductToCartMutation
280
+ );
281
+
282
+ const [
283
+ addSimpleProductToCart,
284
+ { error: errorAddingSimpleProduct, loading: isAddSimpleLoading }
285
+ ] = useMutation(
286
+ addSimpleProductToCartMutation ||
287
+ operations.addSimpleProductToCartMutation
288
+ );
289
+
290
+ const [
291
+ addProductToCart,
292
+ {
293
+ data: addToCartResponseData,
294
+ error: errorAddingProductToCart,
295
+ loading: isAddProductLoading
296
+ }
297
+ ] = useMutation(operations.addProductToCartMutation);
298
+
299
+ const breadcrumbCategoryId = useMemo(
300
+ () => getBreadcrumbCategoryId(product.categories),
301
+ [product.categories]
302
+ );
303
+
304
+ const derivedOptionSelections = useMemo(
305
+ () => deriveOptionSelectionsFromProduct(product),
306
+ [product]
307
+ );
308
+
309
+ const [optionSelections, setOptionSelections] = useState(
310
+ derivedOptionSelections
311
+ );
312
+
313
+ const [singleOptionSelection, setSingleOptionSelection] = useState();
314
+
315
+ const derivedOptionCodes = useMemo(
316
+ () => deriveOptionCodesFromProduct(product),
317
+ [product]
318
+ );
319
+ const [optionCodes] = useState(derivedOptionCodes);
320
+
321
+ const isMissingOptions = useMemo(
322
+ () => getIsMissingOptions(product, optionSelections),
323
+ [product, optionSelections]
324
+ );
325
+
326
+ const isOutOfStock = useMemo(
327
+ () => getIsOutOfStock(product, optionCodes, optionSelections),
328
+ [product, optionCodes, optionSelections]
329
+ );
330
+
331
+ // Check if display out of stock products option is selected in the Admin Dashboard
332
+ const isOutOfStockProductDisplayed = useMemo(() => {
333
+ let totalVariants = 1;
334
+ const isConfigurable = isProductConfigurable(product);
335
+ if (product.configurable_options && isConfigurable) {
336
+ for (const option of product.configurable_options) {
337
+ const length = option.values.length;
338
+ totalVariants = totalVariants * length;
339
+ }
340
+ return product.variants.length === totalVariants;
341
+ }
342
+ }, [product]);
343
+
344
+ const isEverythingOutOfStock = useMemo(() => getIsAllOutOfStock(product), [
345
+ product
346
+ ]);
347
+
348
+ const outOfStockVariants = useMemo(
349
+ () =>
350
+ getOutOfStockVariants(
351
+ product,
352
+ optionCodes,
353
+ singleOptionSelection,
354
+ optionSelections,
355
+ isOutOfStockProductDisplayed
356
+ ),
357
+ [
358
+ product,
359
+ optionCodes,
360
+ singleOptionSelection,
361
+ optionSelections,
362
+ isOutOfStockProductDisplayed
363
+ ]
364
+ );
365
+
366
+ const mediaGalleryEntries = useMemo(
367
+ () => getMediaGalleryEntries(product, optionCodes, optionSelections),
368
+ [product, optionCodes, optionSelections]
369
+ );
370
+
371
+ const customAttributes = useMemo(
372
+ () => getCustomAttributes(product, optionCodes, optionSelections),
373
+ [product, optionCodes, optionSelections]
374
+ );
375
+
376
+ const productPrice = useMemo(
377
+ () => getConfigPrice(product, optionCodes, optionSelections),
378
+ [product, optionCodes, optionSelections]
379
+ );
380
+
381
+ // The map of ids to values (and their uids)
382
+ // For example:
383
+ // { "179" => [{ uid: "abc", value_index: 1 }, { uid: "def", value_index: 2 }]}
384
+ const attributeIdToValuesMap = useMemo(() => {
385
+ const map = new Map();
386
+ // For simple items, this will be an empty map.
387
+ const options = product.configurable_options || [];
388
+ for (const { attribute_id, values } of options) {
389
+ map.set(attribute_id, values);
390
+ }
391
+ return map;
392
+ }, [product.configurable_options]);
393
+
394
+ // An array of selected option uids. Useful for passing to mutations.
395
+ // For example:
396
+ // ["abc", "def"]
397
+ const selectedOptionsArray = useMemo(() => {
398
+ const selectedOptions = [];
399
+
400
+ optionSelections.forEach((value, key) => {
401
+ const values = attributeIdToValuesMap.get(key);
402
+
403
+ const selectedValue = values?.find(
404
+ item => item.value_index === value
405
+ );
406
+
407
+ if (selectedValue) {
408
+ selectedOptions.push(selectedValue.uid);
409
+ }
410
+ });
411
+ return selectedOptions;
412
+ }, [attributeIdToValuesMap, optionSelections]);
413
+
414
+ const handleAddToCart = useCallback(
415
+ async formValues => {
416
+ const { quantity } = formValues;
417
+
418
+ /*
419
+ @deprecated in favor of general addProductsToCart mutation. Will support until the next MAJOR.
420
+ */
421
+ if (hasDeprecatedOperationProp) {
422
+ const payload = {
423
+ item: product,
424
+ productType,
425
+ quantity
426
+ };
427
+
428
+ if (isProductConfigurable(product)) {
429
+ appendOptionsToPayload(
430
+ payload,
431
+ optionSelections,
432
+ optionCodes
433
+ );
434
+ }
435
+
436
+ if (isSupportedProductType) {
437
+ const variables = {
438
+ cartId,
439
+ parentSku: payload.parentSku,
440
+ product: payload.item,
441
+ quantity: payload.quantity,
442
+ sku: payload.item.sku
443
+ };
444
+ // Use the proper mutation for the type.
445
+ if (productType === 'SimpleProduct') {
446
+ try {
447
+ await addSimpleProductToCart({
448
+ variables
449
+ });
450
+ } catch {
451
+ return;
452
+ }
453
+ } else if (productType === 'ConfigurableProduct') {
454
+ try {
455
+ await addConfigurableProductToCart({
456
+ variables
457
+ });
458
+ } catch {
459
+ return;
460
+ }
461
+ }
462
+ } else {
463
+ console.error(
464
+ 'Unsupported product type. Cannot add to cart.'
465
+ );
466
+ }
467
+ } else {
468
+ const variables = {
469
+ cartId,
470
+ product: {
471
+ sku: product.sku,
472
+ quantity
473
+ },
474
+ entered_options: [
475
+ {
476
+ uid: product.uid,
477
+ value: product.name
478
+ }
479
+ ]
480
+ };
481
+
482
+ if (selectedOptionsArray.length) {
483
+ variables.product.selected_options = selectedOptionsArray;
484
+ }
485
+
486
+ try {
487
+ await addProductToCart({ variables });
488
+
489
+ const selectedOptionsLabels =
490
+ selectedOptionsArray?.map((uid, i) => ({
491
+ attribute: product.configurable_options[i].label,
492
+ value:
493
+ product.configurable_options[i].values.findLast(
494
+ x => x.uid === uid
495
+ )?.label || null
496
+ })) || null;
497
+
498
+ dispatch({
499
+ type: 'CART_ADD_ITEM',
500
+ payload: {
501
+ cartId,
502
+ sku: product.sku,
503
+ name: product.name,
504
+ pricing: product.price,
505
+ priceTotal: productPrice.final_price.value,
506
+ currencyCode: productPrice.final_price.currency,
507
+ discountAmount: productPrice.discount.amount_off,
508
+ selectedOptions: selectedOptionsLabels,
509
+ quantity
510
+ }
511
+ });
512
+ } catch {
513
+ return;
514
+ }
515
+ }
516
+ },
517
+ [
518
+ addConfigurableProductToCart,
519
+ addProductToCart,
520
+ addSimpleProductToCart,
521
+ cartId,
522
+ dispatch,
523
+ hasDeprecatedOperationProp,
524
+ isSupportedProductType,
525
+ optionCodes,
526
+ optionSelections,
527
+ product,
528
+ productPrice,
529
+ productType,
530
+ selectedOptionsArray
531
+ ]
532
+ );
533
+
534
+ const handleSelectionChange = useCallback(
535
+ (optionId, selection) => {
536
+ // We must create a new Map here so that React knows that the value
537
+ // of optionSelections has changed.
538
+ const nextOptionSelections = new Map([...optionSelections]);
539
+ nextOptionSelections.set(optionId, selection);
540
+ setOptionSelections(nextOptionSelections);
541
+ // Create a new Map to keep track of single selections with key as String
542
+ const nextSingleOptionSelection = new Map();
543
+ nextSingleOptionSelection.set(optionId, selection);
544
+ setSingleOptionSelection(nextSingleOptionSelection);
545
+ },
546
+ [optionSelections]
547
+ );
548
+
549
+ // Normalization object for product details we need for rendering.
550
+ const productDetails = {
551
+ description: product.description,
552
+ shortDescription: product.short_description,
553
+ name: product.name,
554
+ price: productPrice?.final_price,
555
+ price_range: product?.price_range,
556
+ sku: product.sku,
557
+ term_and_conditions: product.term_and_conditions,
558
+ shipping_policy: product.shipping_policy,
559
+ return_policy: product.return_policy,
560
+ preorder: product.preorder,
561
+ auction_data: product.auction_data
562
+ };
563
+
564
+ const sellerDetails = {
565
+ name: product.seller?.name,
566
+ url_key: product.seller?.url_key,
567
+ city: product.seller?.city,
568
+ country: product.seller?.country
569
+ };
570
+
571
+ const derivedErrorMessage = useMemo(
572
+ () =>
573
+ deriveErrorMessage([
574
+ errorAddingSimpleProduct,
575
+ errorAddingConfigurableProduct,
576
+ errorAddingProductToCart,
577
+ ...(addToCartResponseData?.addProductsToCart?.user_errors || [])
578
+ ]),
579
+ [
580
+ errorAddingConfigurableProduct,
581
+ errorAddingProductToCart,
582
+ errorAddingSimpleProduct,
583
+ addToCartResponseData
584
+ ]
585
+ );
586
+
587
+ const wishlistItemOptions = useMemo(() => {
588
+ const options = {
589
+ quantity: 1,
590
+ sku: product.sku
591
+ };
592
+
593
+ if (productType === 'ConfigurableProduct') {
594
+ options.selected_options = selectedOptionsArray;
595
+ }
596
+
597
+ return options;
598
+ }, [product, productType, selectedOptionsArray]);
599
+
600
+ const wishlistButtonProps = {
601
+ buttonText: isSelected =>
602
+ isSelected
603
+ ? formatMessage({
604
+ id: 'wishlistButton.addedText',
605
+ defaultMessage: 'Added to Favorites'
606
+ })
607
+ : formatMessage({
608
+ id: 'wishlistButton.addText',
609
+ defaultMessage: 'Add to Favorites'
610
+ }),
611
+ item: wishlistItemOptions,
612
+ storeConfig: storeConfigData ? storeConfigData.storeConfig : {}
613
+ };
614
+
615
+ return {
616
+ breadcrumbCategoryId,
617
+ errorMessage: derivedErrorMessage,
618
+ handleAddToCart,
619
+ handleSelectionChange,
620
+ isOutOfStock,
621
+ isEverythingOutOfStock,
622
+ outOfStockVariants,
623
+ isAddToCartDisabled:
624
+ isOutOfStock ||
625
+ isEverythingOutOfStock ||
626
+ isMissingOptions ||
627
+ isAddConfigurableLoading ||
628
+ isAddSimpleLoading ||
629
+ isAddProductLoading,
630
+ isSupportedProductType,
631
+ mediaGalleryEntries,
632
+ shouldShowWishlistButton:
633
+ isSignedIn &&
634
+ storeConfigData &&
635
+ !!storeConfigData.storeConfig.magento_wishlist_general_is_enabled,
636
+ productDetails,
637
+ customAttributes,
638
+ wishlistButtonProps,
639
+ wishlistItemOptions,
640
+ sellerDetails
641
+ };
642
+ };