@magento/peregrine 12.3.0-beta.1 → 12.4.0-beta.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.
Files changed (26) hide show
  1. package/lib/store/reducers/user.js +12 -2
  2. package/lib/talons/CartPage/GiftCards/useGiftCards.js +2 -2
  3. package/lib/talons/CartPage/ProductListing/EditModal/productFormFragment.gql.js +8 -0
  4. package/lib/talons/CartPage/ProductListing/EditModal/useProductForm.js +5 -12
  5. package/lib/talons/CartPage/ProductListing/productListing.gql.ce.js +1 -1
  6. package/lib/talons/CartPage/ProductListing/productListing.gql.ee.js +1 -1
  7. package/lib/talons/CartPage/ProductListing/useQuantity.js +7 -86
  8. package/lib/talons/FilterModal/useFilterModal.js +53 -5
  9. package/lib/talons/FilterModal/useFilterState.js +25 -1
  10. package/lib/talons/FilterSidebar/useFilterSidebar.js +52 -5
  11. package/lib/talons/Gallery/__fixtures__/apolloMocks.js +6 -6
  12. package/lib/talons/Gallery/gallery.gql.ce.js +1 -1
  13. package/lib/talons/Gallery/gallery.gql.ee.js +1 -1
  14. package/lib/talons/ProductFullDetail/productFullDetail.gql.ce.js +1 -1
  15. package/lib/talons/ProductFullDetail/productFullDetail.gql.ee.js +1 -1
  16. package/lib/talons/ProductFullDetail/useProductFullDetail.js +20 -6
  17. package/lib/talons/QuantityStepper/useQuantityStepper.js +122 -0
  18. package/lib/talons/RootComponents/Category/categoryFragments.gql.js +4 -0
  19. package/lib/talons/RootComponents/Product/productDetailFragment.gql.js +19 -0
  20. package/lib/talons/SearchPage/searchPage.gql.js +4 -0
  21. package/lib/talons/WishlistPage/wishlistConfig.gql.ce.js +1 -1
  22. package/lib/talons/WishlistPage/wishlistConfig.gql.ee.js +1 -1
  23. package/lib/util/htmlStringImgUrlConverter.js +26 -0
  24. package/lib/util/resolveLinkProps.js +25 -0
  25. package/lib/util/simplePersistence.js +7 -1
  26. package/package.json +2 -2
@@ -7,7 +7,17 @@ import actions from '../actions/user';
7
7
 
8
8
  export const name = 'user';
9
9
 
10
- const isSignedIn = () => !!storage.getItem('signin_token');
10
+ const rawSignInToken = storage.getRawItem('signin_token');
11
+
12
+ const isSignedIn = () => !!rawSignInToken;
13
+
14
+ const getToken = () => {
15
+ if (!rawSignInToken) {
16
+ return undefined;
17
+ }
18
+ const { value } = JSON.parse(rawSignInToken);
19
+ return value;
20
+ };
11
21
 
12
22
  const initialState = {
13
23
  currentUser: {
@@ -20,7 +30,7 @@ const initialState = {
20
30
  isResettingPassword: false,
21
31
  isSignedIn: isSignedIn(),
22
32
  resetPasswordError: null,
23
- token: storage.getItem('signin_token')
33
+ token: getToken()
24
34
  };
25
35
 
26
36
  const reducerMap = {
@@ -189,7 +189,7 @@ export const useGiftCards = props => {
189
189
  * @property {GraphQLAST} applyGiftCardMutation The mutation used to apply a gift card to the cart.
190
190
  * @property {GraphQLAST} removeGiftCardMutation The mutation used to remove a gift card from the cart.
191
191
  *
192
- * @see [`giftCardQueries.ee.js`]{@link https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/CartPage/GiftCards/giftCardQueries.js}
192
+ * @see [`giftCardQueries.ee.js`]{@link https://github.com/magento/pwa-studio/blob/develop/packages/peregrine/lib/talons/CartPage/GiftCards/giftCardQueries.gql.ee.js}
193
193
  * for queries used in Venia
194
194
  */
195
195
 
@@ -201,7 +201,7 @@ export const useGiftCards = props => {
201
201
  * @property {GraphQLAST} getAppliedGiftCardsQuery The query used to get the gift cards currently applied to the cart.
202
202
  * @property {GraphQLAST} getGiftCardBalanceQuery The query used to get the gift cards currently applied to the cart.
203
203
  *
204
- * @see [`giftCardQueries.ee.js`]{@link https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/CartPage/GiftCards/giftCardQueries.js}
204
+ * @see [`giftCardQueries.ee.js`]{@link https://github.com/magento/pwa-studio/blob/develop/packages/peregrine/lib/talons/CartPage/GiftCards/giftCardQueries.gql.ee.js}
205
205
  * for queries used in Venia
206
206
  */
207
207
 
@@ -43,6 +43,14 @@ export const ProductFormFragment = gql`
43
43
  }
44
44
  }
45
45
  }
46
+ price_range {
47
+ maximum_price {
48
+ final_price {
49
+ currency
50
+ value
51
+ }
52
+ }
53
+ }
46
54
  sku
47
55
  }
48
56
  }
@@ -19,11 +19,11 @@ import DEFAULT_OPERATIONS from './productForm.gql';
19
19
  *
20
20
  * @param {Object} props
21
21
  * @param {Object} props.cartItem The cart item to configure on the form
22
- * @param {GraphQLAST} props.getConfigurableOptionsQuery GraphQL query to get the configurable options for a product.
22
+ * @param {GraphQLDocument} props.getConfigurableOptionsQuery GraphQL query to get the configurable options for a product.
23
23
  * @param {function} props.setIsCartUpdating Function for setting the updating state for the shopping cart.
24
24
  * @param {function} props.setVariantPrice Function for setting the variant price on a product.
25
- * @param {GraphQLAST} props.updateConfigurableOptionsMutation GraphQL mutation for updating the configurable options for a product.
26
- * @param {GraphQLAST} props.updateQuantityMutation GraphQL mutation for updating the quantity of a product in a cart.
25
+ * @param {GraphQLDocument} props.updateConfigurableOptionsMutation GraphQL mutation for updating the configurable options for a product.
26
+ * @param {GraphQLDocument} props.updateQuantityMutation GraphQL mutation for updating the quantity of a product in a cart.
27
27
  * @param {function} props.setActiveEditItem Function for setting the actively editing item.
28
28
  *
29
29
  * @return {ProductFormTalonProps}
@@ -150,15 +150,8 @@ export const useProductForm = props => {
150
150
  }, [storeConfigData]);
151
151
 
152
152
  useEffect(() => {
153
- let variantPrice = null;
154
-
155
- if (selectedVariant) {
156
- const { product } = selectedVariant;
157
- const { price } = product;
158
- const { regularPrice } = price;
159
- variantPrice = regularPrice.amount;
160
- }
161
-
153
+ const variantPrice =
154
+ selectedVariant?.product?.price_range?.maximum_price?.final_price;
162
155
  setVariantPrice(variantPrice);
163
156
  }, [selectedVariant, setVariantPrice]);
164
157
 
@@ -2,7 +2,7 @@ import { gql } from '@apollo/client';
2
2
  import { ProductListingFragment } from './productListingFragments.gql';
3
3
 
4
4
  export const GET_WISHLIST_CONFIG = gql`
5
- query GetWishlistConfigForCartPageCE {
5
+ query GetWishlistConfigForCartPageMOS {
6
6
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
7
7
  storeConfig {
8
8
  store_code
@@ -2,7 +2,7 @@ import { gql } from '@apollo/client';
2
2
  import { ProductListingFragment } from './productListingFragments.gql';
3
3
 
4
4
  export const GET_WISHLIST_CONFIG = gql`
5
- query GetWishlistConfigForCartPageEE {
5
+ query GetWishlistConfigForCartPageAC {
6
6
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
7
7
  storeConfig {
8
8
  store_code
@@ -1,9 +1,7 @@
1
- import { useCallback, useMemo, useState, useEffect } from 'react';
2
- import { useFieldApi } from 'informed';
3
- import useFieldState from '@magento/peregrine/lib/hooks/hook-wrappers/useInformedFieldStateWrapper';
4
- import debounce from 'lodash.debounce';
5
-
6
1
  /**
2
+ *
3
+ * @deprecated - use talons/QuantityStepper/useQuantityStepper instead
4
+ *
7
5
  * This talon contains logic for a product quantity UI component.
8
6
  * It performs effects and returns prop data for rendering a component that lets you
9
7
  * modify the quantity of a cart item.
@@ -24,91 +22,14 @@ import debounce from 'lodash.debounce';
24
22
  * @example <caption>Importing into your project</caption>
25
23
  * import { useQuantity } from '@magento/peregrine/lib/talons/CartPage/ProductListing/useQuantity';
26
24
  */
27
- export const useQuantity = props => {
28
- const { initialValue, min, onChange } = props;
29
-
30
- const [prevQuantity, setPrevQuantity] = useState(initialValue);
31
-
32
- const quantityFieldApi = useFieldApi('quantity');
33
- const { value: quantity } = useFieldState('quantity');
34
-
35
- const isIncrementDisabled = useMemo(() => !quantity, [quantity]);
36
-
37
- // "min: 0" lets a user delete the value and enter a new one, but "1" is
38
- // actually the minimum value we allow to be set through decrement button.
39
- const isDecrementDisabled = useMemo(() => !quantity || quantity <= 1, [
40
- quantity
41
- ]);
42
-
43
- // Fire the onChange after some wait time. We calculate the current delay
44
- // as enough time for a user to spam inc/dec quantity but not enough time
45
- // for a user to click inc/dec on Product A and then click Product B.
46
- const debouncedOnChange = useMemo(
47
- () =>
48
- debounce(val => {
49
- setPrevQuantity(val);
50
- onChange(val);
51
- }, 350),
52
- [onChange]
53
- );
54
-
55
- const handleDecrement = useCallback(() => {
56
- const newQuantity = quantity - 1;
57
- quantityFieldApi.setValue(newQuantity);
58
- debouncedOnChange(newQuantity);
59
- }, [debouncedOnChange, quantity, quantityFieldApi]);
60
-
61
- const handleIncrement = useCallback(() => {
62
- const newQuantity = quantity + 1;
63
- quantityFieldApi.setValue(newQuantity);
64
- debouncedOnChange(newQuantity);
65
- }, [debouncedOnChange, quantity, quantityFieldApi]);
66
-
67
- const handleBlur = useCallback(() => {
68
- // Only submit the value change if it has changed.
69
- if (typeof quantity === 'number' && quantity != prevQuantity) {
70
- debouncedOnChange(quantity);
71
- }
72
- }, [debouncedOnChange, prevQuantity, quantity]);
73
-
74
- const maskInput = useCallback(
75
- value => {
76
- try {
77
- // For some storefronts decimal values are allowed.
78
- const nextVal = parseFloat(value);
79
- if (value && isNaN(nextVal))
80
- throw new Error(`${value} is not a number.`);
81
- if (nextVal < min) return min;
82
- else return nextVal;
83
- } catch (err) {
84
- console.error(err);
85
- return prevQuantity;
86
- }
87
- },
88
- [min, prevQuantity]
89
- );
90
-
91
- /**
92
- * Everytime initialValue changes, update the quantity field state.
93
- */
94
- useEffect(() => {
95
- quantityFieldApi.setValue(initialValue);
96
- }, [initialValue, quantityFieldApi]);
97
-
98
- return {
99
- isDecrementDisabled,
100
- isIncrementDisabled,
101
- handleBlur,
102
- handleDecrement,
103
- handleIncrement,
104
- maskInput
105
- };
106
- };
25
+ export {
26
+ useQuantityStepper as useQuantity
27
+ } from '../../QuantityStepper/useQuantityStepper';
107
28
 
108
29
  /** JSDoc type definitions */
109
30
 
110
31
  /**
111
- * Object type returned by the {@link useQuantity} talon.
32
+ * Object type returned by the {@link useQuantityStepper} talon.
112
33
  * It provides props data for a quantity UI component.
113
34
  *
114
35
  * @typedef {Object} QuantityTalonProps
@@ -82,10 +82,37 @@ export const useFilterModal = props => {
82
82
  return nextFilters;
83
83
  }, [DISABLED_FILTERS, attributeCodes, introspectionData]);
84
84
 
85
+ const isBooleanFilter = options => {
86
+ const optionsString = JSON.stringify(options);
87
+ return (
88
+ options.length <= 2 &&
89
+ (optionsString.includes(
90
+ JSON.stringify({
91
+ __typename: 'AggregationOption',
92
+ label: '0',
93
+ value: '0'
94
+ })
95
+ ) ||
96
+ optionsString.includes(
97
+ JSON.stringify({
98
+ __typename: 'AggregationOption',
99
+ label: '1',
100
+ value: '1'
101
+ })
102
+ ))
103
+ );
104
+ };
105
+
85
106
  // iterate over filters once to set up all the collections we need
86
- const [filterNames, filterKeys, filterItems] = useMemo(() => {
107
+ const [
108
+ filterNames,
109
+ filterKeys,
110
+ filterItems,
111
+ filterFrontendInput
112
+ ] = useMemo(() => {
87
113
  const names = new Map();
88
114
  const keys = new Set();
115
+ const frontendInput = new Map();
89
116
  const itemsByGroup = new Map();
90
117
 
91
118
  const sortedFilters = sortFiltersArray([...filters]);
@@ -103,15 +130,35 @@ export const useFilterModal = props => {
103
130
  // add filter key permutations
104
131
  keys.add(`${group}[filter]`);
105
132
 
106
- // add items
107
- for (const { label, value } of options) {
108
- items.push({ title: stripHtml(label), value });
133
+ // TODO: Get all frontend input type from gql if other filter input types are needed
134
+ // See: https://github.com/magento-commerce/magento2-pwa/pull/26
135
+ if (isBooleanFilter(options)) {
136
+ frontendInput.set(group, 'boolean');
137
+ // add items
138
+ items.push({
139
+ title: 'No',
140
+ value: '0',
141
+ label: name + ':' + 'No'
142
+ });
143
+ items.push({
144
+ title: 'Yes',
145
+ value: '1',
146
+ label: name + ':' + 'Yes'
147
+ });
148
+ } else {
149
+ // Add frontend input type
150
+ frontendInput.set(group, null);
151
+ // add items
152
+ for (const { label, value } of options) {
153
+ items.push({ title: stripHtml(label), value });
154
+ }
109
155
  }
156
+
110
157
  itemsByGroup.set(group, items);
111
158
  }
112
159
  }
113
160
 
114
- return [names, keys, itemsByGroup];
161
+ return [names, keys, itemsByGroup, frontendInput];
115
162
  }, [filters, possibleFilters]);
116
163
 
117
164
  // on apply, write filter state to location
@@ -196,6 +243,7 @@ export const useFilterModal = props => {
196
243
  filterItems,
197
244
  filterKeys,
198
245
  filterNames,
246
+ filterFrontendInput,
199
247
  filterState,
200
248
  handleApply,
201
249
  handleClose,
@@ -37,6 +37,14 @@ const reducer = (state, action) => {
37
37
 
38
38
  return nextState;
39
39
  }
40
+ case 'remove group': {
41
+ const { group } = payload;
42
+ const nextState = new Map(state);
43
+
44
+ nextState.delete(group);
45
+
46
+ return nextState;
47
+ }
40
48
  case 'toggle item': {
41
49
  const { group, item } = payload;
42
50
  const nextState = new Map(state);
@@ -89,6 +97,13 @@ export const useFilterState = () => {
89
97
  [dispatch]
90
98
  );
91
99
 
100
+ const removeGroup = useCallback(
101
+ payload => {
102
+ dispatch({ payload, type: 'remove group' });
103
+ },
104
+ [dispatch]
105
+ );
106
+
92
107
  const setItems = useCallback(
93
108
  payload => {
94
109
  dispatch({ payload, type: 'set items' });
@@ -109,10 +124,19 @@ export const useFilterState = () => {
109
124
  clear,
110
125
  dispatch,
111
126
  removeItem,
127
+ removeGroup,
112
128
  setItems,
113
129
  toggleItem
114
130
  }),
115
- [addItem, clear, dispatch, removeItem, setItems, toggleItem]
131
+ [
132
+ addItem,
133
+ clear,
134
+ dispatch,
135
+ removeItem,
136
+ removeGroup,
137
+ setItems,
138
+ toggleItem
139
+ ]
116
140
  );
117
141
 
118
142
  return [state, api];
@@ -72,10 +72,37 @@ export const useFilterSidebar = props => {
72
72
  return nextFilters;
73
73
  }, [DISABLED_FILTERS, attributeCodes, introspectionData]);
74
74
 
75
+ const isBooleanFilter = options => {
76
+ const optionsString = JSON.stringify(options);
77
+ return (
78
+ options.length <= 2 &&
79
+ (optionsString.includes(
80
+ JSON.stringify({
81
+ __typename: 'AggregationOption',
82
+ label: '0',
83
+ value: '0'
84
+ })
85
+ ) ||
86
+ optionsString.includes(
87
+ JSON.stringify({
88
+ __typename: 'AggregationOption',
89
+ label: '1',
90
+ value: '1'
91
+ })
92
+ ))
93
+ );
94
+ };
95
+
75
96
  // iterate over filters once to set up all the collections we need
76
- const [filterNames, filterKeys, filterItems] = useMemo(() => {
97
+ const [
98
+ filterNames,
99
+ filterKeys,
100
+ filterItems,
101
+ filterFrontendInput
102
+ ] = useMemo(() => {
77
103
  const names = new Map();
78
104
  const keys = new Set();
105
+ const frontendInput = new Map();
79
106
  const itemsByGroup = new Map();
80
107
 
81
108
  const sortedFilters = sortFiltersArray([...filters]);
@@ -93,15 +120,34 @@ export const useFilterSidebar = props => {
93
120
  // add filter key permutations
94
121
  keys.add(`${group}[filter]`);
95
122
 
96
- // add items
97
- for (const { label, value } of options) {
98
- items.push({ title: stripHtml(label), value });
123
+ // TODO: Get all frontend input type from gql if other filter input types are needed
124
+ // See https://github.com/magento-commerce/magento2-pwa/pull/26
125
+ if (isBooleanFilter(options)) {
126
+ frontendInput.set(group, 'boolean');
127
+ // add items
128
+ items.push({
129
+ title: 'No',
130
+ value: '0',
131
+ label: name + ':' + 'No'
132
+ });
133
+ items.push({
134
+ title: 'Yes',
135
+ value: '1',
136
+ label: name + ':' + 'Yes'
137
+ });
138
+ } else {
139
+ // Add frontend input type
140
+ frontendInput.set(group, null);
141
+ // add items
142
+ for (const { label, value } of options) {
143
+ items.push({ title: stripHtml(label), value });
144
+ }
99
145
  }
100
146
  itemsByGroup.set(group, items);
101
147
  }
102
148
  }
103
149
 
104
- return [names, keys, itemsByGroup];
150
+ return [names, keys, itemsByGroup, frontendInput];
105
151
  }, [filters, possibleFilters]);
106
152
 
107
153
  // on apply, write filter state to location
@@ -192,6 +238,7 @@ export const useFilterSidebar = props => {
192
238
  filterItems,
193
239
  filterKeys,
194
240
  filterNames,
241
+ filterFrontendInput,
195
242
  filterState,
196
243
  handleApply,
197
244
  handleClose,
@@ -1,9 +1,9 @@
1
- import eeOperations from '../gallery.gql.ee';
2
- import ceOperations from '../gallery.gql.ce';
1
+ import acOperations from '../gallery.gql.ee';
2
+ import mosOperations from '../gallery.gql.ce';
3
3
 
4
- export const mockGetStoreConfigEE = {
4
+ export const mockGetStoreConfigAC = {
5
5
  request: {
6
- query: eeOperations.getStoreConfigQuery
6
+ query: acOperations.getStoreConfigQuery
7
7
  },
8
8
  result: {
9
9
  data: {
@@ -17,9 +17,9 @@ export const mockGetStoreConfigEE = {
17
17
  }
18
18
  };
19
19
 
20
- export const mockGetStoreConfigCE = {
20
+ export const mockGetStoreConfigMOS = {
21
21
  request: {
22
- query: ceOperations.getStoreConfigQuery
22
+ query: mosOperations.getStoreConfigQuery
23
23
  },
24
24
  result: {
25
25
  data: {
@@ -1,7 +1,7 @@
1
1
  import { gql } from '@apollo/client';
2
2
 
3
3
  export const GET_STORE_CONFIG_DATA = gql`
4
- query GetStoreConfigDataForGalleryCE {
4
+ query GetStoreConfigDataForGalleryMOS {
5
5
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
6
6
  storeConfig {
7
7
  store_code
@@ -1,7 +1,7 @@
1
1
  import { gql } from '@apollo/client';
2
2
 
3
3
  export const GET_STORE_CONFIG_DATA = gql`
4
- query GetStoreConfigDataForGalleryEE {
4
+ query GetStoreConfigDataForGalleryAC {
5
5
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
6
6
  storeConfig {
7
7
  store_code
@@ -18,7 +18,7 @@ export const ADD_PRODUCT_TO_CART = gql`
18
18
  `;
19
19
 
20
20
  export const GET_WISHLIST_CONFIG = gql`
21
- query GetWishlistConfigForProductCE {
21
+ query GetWishlistConfigForProductMOS {
22
22
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
23
23
  storeConfig {
24
24
  store_code
@@ -3,7 +3,7 @@ import { gql } from '@apollo/client';
3
3
  import defaultOperations from './productFullDetail.gql.ce';
4
4
 
5
5
  export const GET_WISHLIST_CONFIG = gql`
6
- query GetWishlistConfigForProductEE {
6
+ query GetWishlistConfigForProductAC {
7
7
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
8
8
  storeConfig {
9
9
  store_code
@@ -78,8 +78,9 @@ const getIsOutOfStock = (product, optionCodes, optionSelections) => {
78
78
  optionSelections,
79
79
  variants
80
80
  });
81
+ const stockStatus = item?.product?.stock_status;
81
82
 
82
- return item.product.stock_status === OUT_OF_STOCK_CODE;
83
+ return stockStatus === OUT_OF_STOCK_CODE || !stockStatus;
83
84
  }
84
85
  return stock_status === OUT_OF_STOCK_CODE;
85
86
  };
@@ -154,7 +155,7 @@ const getConfigPrice = (product, optionCodes, optionSelections) => {
154
155
  0;
155
156
 
156
157
  if (!isConfigurable || !optionsSelected) {
157
- value = product.price.regularPrice.amount;
158
+ value = product.price_range?.maximum_price?.final_price;
158
159
  } else {
159
160
  const item = findMatchingVariant({
160
161
  optionCodes,
@@ -163,13 +164,21 @@ const getConfigPrice = (product, optionCodes, optionSelections) => {
163
164
  });
164
165
 
165
166
  value = item
166
- ? item.product.price.regularPrice.amount
167
- : product.price.regularPrice.amount;
167
+ ? item.product.price_range?.maximum_price?.final_price
168
+ : product.price_range?.maximum_price?.final_price;
168
169
  }
169
170
 
170
171
  return value;
171
172
  };
172
173
 
174
+ const attributeLabelCompare = (attribute1, attribute2) => {
175
+ const label1 = attribute1['attribute_metadata']['label'].toLowerCase();
176
+ const label2 = attribute2['attribute_metadata']['label'].toLowerCase();
177
+ if (label1 < label2) return -1;
178
+ else if (label1 > label2) return 1;
179
+ else return 0;
180
+ };
181
+
173
182
  const getCustomAttributes = (product, optionCodes, optionSelections) => {
174
183
  const { custom_attributes, variants } = product;
175
184
  const isConfigurable = isProductConfigurable(product);
@@ -184,10 +193,14 @@ const getCustomAttributes = (product, optionCodes, optionSelections) => {
184
193
  variants
185
194
  });
186
195
 
187
- return item.product.custom_attributes;
196
+ return item && item.product
197
+ ? [...item.product.custom_attributes].sort(attributeLabelCompare)
198
+ : [];
188
199
  }
189
200
 
190
- return custom_attributes;
201
+ return custom_attributes
202
+ ? [...custom_attributes].sort(attributeLabelCompare)
203
+ : [];
191
204
  };
192
205
 
193
206
  /**
@@ -447,6 +460,7 @@ export const useProductFullDetail = props => {
447
460
  // Normalization object for product details we need for rendering.
448
461
  const productDetails = {
449
462
  description: product.description,
463
+ shortDescription: product.short_description,
450
464
  name: product.name,
451
465
  price: productPrice,
452
466
  sku: product.sku
@@ -0,0 +1,122 @@
1
+ import { useCallback, useMemo, useState, useEffect } from 'react';
2
+ import { useFieldApi } from 'informed';
3
+ import useFieldState from '@magento/peregrine/lib/hooks/hook-wrappers/useInformedFieldStateWrapper';
4
+ import debounce from 'lodash.debounce';
5
+
6
+ /**
7
+ * This talon contains logic for a product quantity UI component.
8
+ * It performs effects and returns prop data for rendering a component that lets you
9
+ * modify the quantity of a cart item.
10
+ *
11
+ * This talon performs the following effects:
12
+ *
13
+ * - Updates the state of the quantity field when the initial value changes
14
+ *
15
+ * @function
16
+ *
17
+ * @param {Object} props
18
+ * @param {number} props.initialValue the initial quantity value
19
+ * @param {number} props.min the minimum allowed quantity value
20
+ * @param {function} props.onChange change handler to invoke when quantity value changes
21
+ *
22
+ * @returns {QuantityTalonProps}
23
+ *
24
+ * @example <caption>Importing into your project</caption>
25
+ * import { useQuantityStepper } from '@magento/peregrine/lib/talons/CartPage/ProductListing/useQuantityStepper';
26
+ */
27
+ export const useQuantityStepper = props => {
28
+ const { initialValue, min, onChange } = props;
29
+
30
+ const [prevQuantity, setPrevQuantity] = useState(initialValue);
31
+
32
+ const quantityFieldApi = useFieldApi('quantity');
33
+ const { value: quantity } = useFieldState('quantity');
34
+
35
+ const isIncrementDisabled = useMemo(() => !quantity, [quantity]);
36
+
37
+ // "min: 0" lets a user delete the value and enter a new one, but "1" is
38
+ // actually the minimum value we allow to be set through decrement button.
39
+ const isDecrementDisabled = useMemo(() => !quantity || quantity <= 1, [
40
+ quantity
41
+ ]);
42
+
43
+ // Fire the onChange after some wait time. We calculate the current delay
44
+ // as enough time for a user to spam inc/dec quantity but not enough time
45
+ // for a user to click inc/dec on Product A and then click Product B.
46
+ const debouncedOnChange = useMemo(
47
+ () =>
48
+ debounce(val => {
49
+ setPrevQuantity(val);
50
+ onChange(val);
51
+ }, 350),
52
+ [onChange]
53
+ );
54
+
55
+ const handleDecrement = useCallback(() => {
56
+ const newQuantity = quantity - 1;
57
+ quantityFieldApi.setValue(newQuantity);
58
+ debouncedOnChange(newQuantity);
59
+ }, [debouncedOnChange, quantity, quantityFieldApi]);
60
+
61
+ const handleIncrement = useCallback(() => {
62
+ const newQuantity = quantity + 1;
63
+ quantityFieldApi.setValue(newQuantity);
64
+ debouncedOnChange(newQuantity);
65
+ }, [debouncedOnChange, quantity, quantityFieldApi]);
66
+
67
+ const handleBlur = useCallback(() => {
68
+ // Only submit the value change if it has changed.
69
+ if (typeof quantity === 'number' && quantity != prevQuantity) {
70
+ debouncedOnChange(quantity);
71
+ }
72
+ }, [debouncedOnChange, prevQuantity, quantity]);
73
+
74
+ const maskInput = useCallback(
75
+ value => {
76
+ try {
77
+ // For some storefronts decimal values are allowed.
78
+ const nextVal = parseFloat(value);
79
+ if (value && isNaN(nextVal))
80
+ throw new Error(`${value} is not a number.`);
81
+ if (nextVal < min) return min;
82
+ else return nextVal;
83
+ } catch (err) {
84
+ console.error(err);
85
+ return prevQuantity;
86
+ }
87
+ },
88
+ [min, prevQuantity]
89
+ );
90
+
91
+ /**
92
+ * Everytime initialValue changes, update the quantity field state.
93
+ */
94
+ useEffect(() => {
95
+ quantityFieldApi.setValue(initialValue);
96
+ }, [initialValue, quantityFieldApi]);
97
+
98
+ return {
99
+ isDecrementDisabled,
100
+ isIncrementDisabled,
101
+ handleBlur,
102
+ handleDecrement,
103
+ handleIncrement,
104
+ maskInput
105
+ };
106
+ };
107
+
108
+ /** JSDoc type definitions */
109
+
110
+ /**
111
+ * Object type returned by the {@link useQuantityStepper} talon.
112
+ * It provides props data for a quantity UI component.
113
+ *
114
+ * @typedef {Object} QuantityTalonProps
115
+ *
116
+ * @property {boolean} isDecrementDisabled True if decrementing should be disabled
117
+ * @property {boolean} isIncrementDisabled True if incrementing should be disabled
118
+ * @property {function} handleBlur Callback function for handling a blur event on a component
119
+ * @property {function} handleDecrement Callback function for handling a quantity decrement event
120
+ * @property {function} handleIncrement Callback function for handling an increment event
121
+ * @property {function} maskInput Function for masking a value when decimal values are allowed
122
+ */
@@ -18,6 +18,10 @@ export const ProductsFragment = gql`
18
18
  name
19
19
  price_range {
20
20
  maximum_price {
21
+ final_price {
22
+ currency
23
+ value
24
+ }
21
25
  regular_price {
22
26
  currency
23
27
  value
@@ -13,6 +13,9 @@ export const ProductDetailsFragment = gql`
13
13
  description {
14
14
  html
15
15
  }
16
+ short_description {
17
+ html
18
+ }
16
19
  id
17
20
  uid
18
21
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
@@ -33,6 +36,14 @@ export const ProductDetailsFragment = gql`
33
36
  }
34
37
  }
35
38
  }
39
+ price_range {
40
+ maximum_price {
41
+ final_price {
42
+ currency
43
+ value
44
+ }
45
+ }
46
+ }
36
47
  sku
37
48
  small_image {
38
49
  url
@@ -119,6 +130,14 @@ export const ProductDetailsFragment = gql`
119
130
  }
120
131
  }
121
132
  }
133
+ price_range {
134
+ maximum_price {
135
+ final_price {
136
+ currency
137
+ value
138
+ }
139
+ }
140
+ }
122
141
  custom_attributes {
123
142
  selected_attribute_options {
124
143
  attribute_option {
@@ -48,6 +48,10 @@ export const PRODUCT_SEARCH = gql`
48
48
  name
49
49
  price_range {
50
50
  maximum_price {
51
+ final_price {
52
+ currency
53
+ value
54
+ }
51
55
  regular_price {
52
56
  currency
53
57
  value
@@ -1,7 +1,7 @@
1
1
  import { gql } from '@apollo/client';
2
2
 
3
3
  export const GET_WISHLIST_CONFIG = gql`
4
- query GetWishlistConfigForWishlistPageCE {
4
+ query GetWishlistConfigForWishlistPageMOS {
5
5
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
6
6
  storeConfig {
7
7
  store_code
@@ -1,7 +1,7 @@
1
1
  import { gql } from '@apollo/client';
2
2
 
3
3
  export const GET_WISHLIST_CONFIG = gql`
4
- query GetWishlistConfigForWishlistPageEE {
4
+ query GetWishlistConfigForWishlistPageAC {
5
5
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
6
6
  storeConfig {
7
7
  store_code
@@ -0,0 +1,26 @@
1
+ import makeUrl from './makeUrl';
2
+ import resolveLinkProps from './resolveLinkProps';
3
+
4
+ /**
5
+ * Modifies html string images to use makeUrl as source and resolves links to use internal path.
6
+ *
7
+ * @param {string} htmlString - the html string to be updated
8
+ * @return {string}
9
+ */
10
+ const htmlStringImgUrlConverter = htmlString => {
11
+ const temporaryElement = document.createElement('div');
12
+ temporaryElement.innerHTML = htmlString;
13
+ for (const imgElement of temporaryElement.getElementsByTagName('img')) {
14
+ imgElement.src = makeUrl(imgElement.src, {
15
+ type: 'image-wysiwyg',
16
+ quality: 85
17
+ });
18
+ }
19
+ for (const linkElement of temporaryElement.getElementsByTagName('a')) {
20
+ const linkProps = resolveLinkProps(linkElement.href);
21
+ linkElement.href = linkProps.to || linkProps.href;
22
+ }
23
+ return temporaryElement.innerHTML;
24
+ };
25
+
26
+ export default htmlStringImgUrlConverter;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Resolve link properties
3
+ *
4
+ * @param {string} link
5
+ */
6
+ export default link => {
7
+ let isExternalUrl;
8
+ const linkProps = {};
9
+
10
+ try {
11
+ const baseUrlObj = new URL(process.env.MAGENTO_BACKEND_URL);
12
+ const urlObj = new URL(link, baseUrlObj);
13
+ isExternalUrl = baseUrlObj.host !== urlObj.host;
14
+
15
+ if (isExternalUrl) {
16
+ linkProps['href'] = link;
17
+ } else {
18
+ linkProps['to'] = urlObj.pathname;
19
+ }
20
+ } catch (e) {
21
+ linkProps['href'] = link;
22
+ }
23
+
24
+ return linkProps;
25
+ };
@@ -42,11 +42,17 @@ export default class BrowserPersistence {
42
42
  return this.storage.getItem(name);
43
43
  }
44
44
  getItem(name) {
45
+ const now = Date.now();
45
46
  const item = this.storage.getItem(name);
46
47
  if (!item) {
47
48
  return undefined;
48
49
  }
49
- const { value } = JSON.parse(item);
50
+ const { value, ttl, timeStored } = JSON.parse(item);
51
+
52
+ if (ttl && now - timeStored > ttl * 1000) {
53
+ this.storage.removeItem(name);
54
+ return undefined;
55
+ }
50
56
 
51
57
  return JSON.parse(value);
52
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magento/peregrine",
3
- "version": "12.3.0-beta.1",
3
+ "version": "12.4.0-beta.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -10,7 +10,7 @@
10
10
  "clean": " "
11
11
  },
12
12
  "repository": "github:magento/pwa-studio",
13
- "author": "Magento Commerce",
13
+ "author": "Adobe Commerce",
14
14
  "license": "(OSL-3.0 OR AFL-3.0)",
15
15
  "bugs": {
16
16
  "url": "https://github.com/magento/pwa-studio/issues"