@magento/peregrine 12.3.0-alpha.1 → 12.4.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.
- package/lib/store/reducers/user.js +12 -2
- package/lib/talons/CartPage/GiftCards/useGiftCards.js +2 -2
- package/lib/talons/CartPage/ProductListing/EditModal/productFormFragment.gql.js +8 -0
- package/lib/talons/CartPage/ProductListing/EditModal/useProductForm.js +5 -12
- package/lib/talons/CartPage/ProductListing/productListing.gql.ce.js +1 -1
- package/lib/talons/CartPage/ProductListing/productListing.gql.ee.js +1 -1
- package/lib/talons/CartPage/ProductListing/useQuantity.js +7 -86
- package/lib/talons/FilterModal/useFilterModal.js +53 -5
- package/lib/talons/FilterModal/useFilterState.js +25 -1
- package/lib/talons/FilterSidebar/useFilterSidebar.js +52 -5
- package/lib/talons/Gallery/__fixtures__/apolloMocks.js +6 -6
- package/lib/talons/Gallery/gallery.gql.ce.js +1 -1
- package/lib/talons/Gallery/gallery.gql.ee.js +1 -1
- package/lib/talons/ProductFullDetail/productFullDetail.gql.ce.js +1 -1
- package/lib/talons/ProductFullDetail/productFullDetail.gql.ee.js +1 -1
- package/lib/talons/ProductFullDetail/useProductFullDetail.js +20 -6
- package/lib/talons/QuantityStepper/useQuantityStepper.js +122 -0
- package/lib/talons/RootComponents/Category/categoryFragments.gql.js +1 -1
- package/lib/talons/RootComponents/Product/productDetailFragment.gql.js +19 -0
- package/lib/talons/SearchPage/searchPage.gql.js +1 -1
- package/lib/talons/WishlistPage/wishlistConfig.gql.ce.js +1 -1
- package/lib/talons/WishlistPage/wishlistConfig.gql.ee.js +1 -1
- package/lib/util/htmlStringImgUrlConverter.js +26 -0
- package/lib/util/resolveLinkProps.js +25 -0
- package/lib/util/simplePersistence.js +7 -1
- 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
|
|
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:
|
|
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/
|
|
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/
|
|
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
|
|
|
@@ -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 {
|
|
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 {
|
|
26
|
-
* @param {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
28
|
-
|
|
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
|
|
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 [
|
|
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
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
[
|
|
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 [
|
|
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
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
2
|
-
import
|
|
1
|
+
import acOperations from '../gallery.gql.ee';
|
|
2
|
+
import mosOperations from '../gallery.gql.ce';
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const mockGetStoreConfigAC = {
|
|
5
5
|
request: {
|
|
6
|
-
query:
|
|
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
|
|
20
|
+
export const mockGetStoreConfigMOS = {
|
|
21
21
|
request: {
|
|
22
|
-
query:
|
|
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
|
|
4
|
+
query GetStoreConfigDataForGalleryMOS {
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
167
|
-
: product.
|
|
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
|
|
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
|
+
*/
|
|
@@ -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 {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { gql } from '@apollo/client';
|
|
2
2
|
|
|
3
3
|
export const GET_WISHLIST_CONFIG = gql`
|
|
4
|
-
query
|
|
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
|
|
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
|
+
"version": "12.4.0-alpha.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": "
|
|
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"
|