@shopgate/pwa-common 7.32.0-beta.5 → 7.32.0-beta.6
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/components/Image/Image.d.ts +78 -0
- package/components/Image/Image.d.ts.map +1 -0
- package/components/Image/Image.js +123 -91
- package/components/Image/ImageInner.d.ts +45 -0
- package/components/Image/ImageInner.d.ts.map +1 -0
- package/components/Image/ImageInner.js +23 -29
- package/components/Image/index.d.ts +3 -0
- package/components/Image/index.d.ts.map +1 -0
- package/constants/Configuration.js +7 -0
- package/package.json +3 -3
- package/providers/toast/index.js +54 -40
- package/providers/toast/spec.js +110 -0
- package/subscriptions/error.js +50 -27
- package/subscriptions/helpers/pipeline.js +68 -0
- package/styles/reset/form.js +0 -58
- package/styles/reset/index.js +0 -8
- package/styles/reset/media.js +0 -26
- package/styles/reset/root.js +0 -37
- package/styles/reset/table.js +0 -14
- package/styles/reset/typography.js +0 -30
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ReactElement, SyntheticEvent } from 'react';
|
|
2
|
+
import type { ImageResolution } from '@shopgate/engage/settings/types/appSettings';
|
|
3
|
+
export interface ImageProps {
|
|
4
|
+
/**
|
|
5
|
+
* Optional alt text for the image. Without one the image is marked decorative.
|
|
6
|
+
*/
|
|
7
|
+
alt?: string | null;
|
|
8
|
+
/**
|
|
9
|
+
* The background color of the image container.
|
|
10
|
+
*/
|
|
11
|
+
backgroundColor?: string;
|
|
12
|
+
/**
|
|
13
|
+
* External class name for the image container.
|
|
14
|
+
*/
|
|
15
|
+
className?: string | null;
|
|
16
|
+
/**
|
|
17
|
+
* External class name for the image itself.
|
|
18
|
+
*/
|
|
19
|
+
classNameImg?: string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Shows the `placeholder` even when the image could be loaded. For a parent that decides on
|
|
22
|
+
* grounds this component cannot see, e.g. a product that is not orderable.
|
|
23
|
+
*/
|
|
24
|
+
forcePlaceholder?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Callback that is invoked when the image with the highest resolution has been loaded.
|
|
27
|
+
*/
|
|
28
|
+
highestResolutionLoaded?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Microdata property name, e.g. "image" for a schema.org Product. Emitted as the itemprop
|
|
31
|
+
* attribute so crawlers can pick the image out of the surrounding item.
|
|
32
|
+
*/
|
|
33
|
+
itemProp?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the image is loaded only once it comes near the viewport. Turn it off for images that
|
|
36
|
+
* are visible immediately, e.g. the first tile of a list.
|
|
37
|
+
*/
|
|
38
|
+
lazy?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Callback that is invoked when image loading failed. Handling the failure is not required - a
|
|
41
|
+
* `placeholder` covers that - so this is for callers that need to react beyond the image itself.
|
|
42
|
+
*/
|
|
43
|
+
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Callback that is invoked when the image has been loaded.
|
|
46
|
+
*/
|
|
47
|
+
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Rendered in place of the image when there is no `src`, when `forcePlaceholder` is set, or when
|
|
50
|
+
* the image failed to load. It renders inside the container, so it inherits the reserved aspect
|
|
51
|
+
* ratio box and needs no sizing of its own.
|
|
52
|
+
*/
|
|
53
|
+
placeholder?: ReactElement | null;
|
|
54
|
+
/**
|
|
55
|
+
* Width and height parts of the rendered aspect ratio, e.g. [16, 9]. Without one the ratio is
|
|
56
|
+
* calculated from the highest resolution.
|
|
57
|
+
*/
|
|
58
|
+
ratio?: number[] | null;
|
|
59
|
+
/**
|
|
60
|
+
* The resolutions to request, ordered ascending. The last is displayed; the second to last is
|
|
61
|
+
* loaded first as a preview, so something appears as soon as possible.
|
|
62
|
+
*/
|
|
63
|
+
resolutions?: ImageResolution[];
|
|
64
|
+
/**
|
|
65
|
+
* Image source. A url the image service serves is rewritten to carry the requested dimensions and
|
|
66
|
+
* the app wide quality and fill settings. Any other url is used as it is, so `resolutions` and
|
|
67
|
+
* `ratio` then only affect the layout, not what is downloaded.
|
|
68
|
+
*/
|
|
69
|
+
src?: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Renders a plain img tag without the container, for callers that position the image themselves.
|
|
72
|
+
* Nothing reserves its space while it loads, and `backgroundColor` has nothing to apply to.
|
|
73
|
+
*/
|
|
74
|
+
unwrapped?: boolean;
|
|
75
|
+
}
|
|
76
|
+
declare const _default: import("react").MemoExoticComponent<(props: ImageProps) => import("react").JSX.Element | null>;
|
|
77
|
+
export default _default;
|
|
78
|
+
//# sourceMappingURL=Image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../../../components/Image/Image.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,YAAY,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAKzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAGnF,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;OAEG;IACH,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAC5D;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAC3D;;;;OAIG;IACH,WAAW,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;oEA8EqB,UAAU;AAoMhC,wBAA2B"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import PropTypes from 'prop-types';
|
|
1
|
+
import { useMemo, useState, useEffect, useRef, useCallback, memo } from 'react';
|
|
3
2
|
import noop from 'lodash/noop';
|
|
4
3
|
import { makeStyles } from '@shopgate/engage/styles';
|
|
5
4
|
import { getFullImageSource } from '@shopgate/engage/core/helpers';
|
|
5
|
+
import { useImageServiceSettings } from '@shopgate/engage/settings/hooks';
|
|
6
6
|
import ImageInner from "./ImageInner";
|
|
7
7
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
8
|
const useStyles = makeStyles()((_theme, {
|
|
@@ -23,57 +23,112 @@ const useStyles = makeStyles()((_theme, {
|
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Calculates the Greatest Common Divisor
|
|
27
|
-
*
|
|
28
|
-
* @param
|
|
29
|
-
* @
|
|
30
|
-
* @returns {number} The greatest common divisor of `a` and `b`.
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* gcd(1920, 1080); // Returns 120
|
|
34
|
-
* gcd(10, 15); // Returns 5
|
|
35
|
-
* gcd(100, 25); // Returns 25
|
|
26
|
+
* Calculates the Greatest Common Divisor of two numbers using the Euclidean algorithm.
|
|
27
|
+
* @param a The first number.
|
|
28
|
+
* @param b The second number.
|
|
29
|
+
* @returns The greatest common divisor, or 1 when the pair cannot be reduced.
|
|
36
30
|
*/
|
|
37
|
-
const gcd = (a, b) =>
|
|
31
|
+
const gcd = (a, b) => {
|
|
32
|
+
// Anything but a pair of positive whole numbers is left unreduced. The values can originate from
|
|
33
|
+
// an admin field a merchant is still typing into, and NaN or Infinity would otherwise recurse
|
|
34
|
+
// until the stack overflows, while a fraction would reduce to a near-zero divisor.
|
|
35
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || a <= 0 || b <= 0) {
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
let dividend = a;
|
|
39
|
+
let remainder = b;
|
|
40
|
+
while (remainder !== 0) {
|
|
41
|
+
const next = dividend % remainder;
|
|
42
|
+
dividend = remainder;
|
|
43
|
+
remainder = next;
|
|
44
|
+
}
|
|
45
|
+
return dividend;
|
|
46
|
+
};
|
|
38
47
|
|
|
39
48
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param
|
|
42
|
-
* @returns
|
|
49
|
+
* Whether a value can be used as one side of an aspect ratio.
|
|
50
|
+
* @param value The value to check.
|
|
51
|
+
* @returns Whether it is a usable length.
|
|
52
|
+
*/
|
|
53
|
+
const isUsableLength = value => Number.isFinite(value) && value > 0;
|
|
54
|
+
const defaultResolutions = [{
|
|
55
|
+
width: 440,
|
|
56
|
+
height: 440
|
|
57
|
+
}];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Renders an image at the configured resolutions, inside a container that reserves its aspect ratio
|
|
61
|
+
* so the layout does not shift while it loads.
|
|
62
|
+
*
|
|
63
|
+
* What a caller should know:
|
|
64
|
+
*
|
|
65
|
+
* - **Nothing is rendered until there is something to render.** Without a `src`, the container is
|
|
66
|
+
* still there and keeps its space, but it stays empty unless a `placeholder` is given.
|
|
67
|
+
* - **A failed image is remembered per source, not per component.** Once a `placeholder` is given,
|
|
68
|
+
* it takes over on failure, and the image returns by itself as soon as the requested url changes -
|
|
69
|
+
* a new `src`, different `resolutions`, or an app wide image setting edited in the admin. Callers
|
|
70
|
+
* that pass no `placeholder` keep the failed image, and `onError` keeps reporting every attempt.
|
|
71
|
+
* - **`onLoad` and `onError` are always forwarded**, whether or not a placeholder is involved.
|
|
72
|
+
* - **`unwrapped` gives up the container**, and with it the reserved aspect ratio box and the
|
|
73
|
+
* background color. The caller has to provide the space itself.
|
|
74
|
+
* @param props The components props.
|
|
75
|
+
* @returns The rendered component.
|
|
43
76
|
*/
|
|
44
|
-
const Image =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
const Image = props => {
|
|
78
|
+
const {
|
|
79
|
+
alt = null,
|
|
80
|
+
backgroundColor = 'var(--sg-palette-background-emphasized)',
|
|
81
|
+
className = '',
|
|
82
|
+
classNameImg = '',
|
|
83
|
+
forcePlaceholder: parentRendersPlaceholder = false,
|
|
84
|
+
highestResolutionLoaded = noop,
|
|
85
|
+
itemProp,
|
|
86
|
+
onError = noop,
|
|
87
|
+
onLoad = noop,
|
|
88
|
+
placeholder = null,
|
|
89
|
+
ratio = null,
|
|
90
|
+
resolutions = defaultResolutions,
|
|
91
|
+
src = null,
|
|
92
|
+
lazy = true,
|
|
93
|
+
unwrapped = false
|
|
94
|
+
} = props;
|
|
95
|
+
|
|
96
|
+
// Quality and fill color are configured app wide, so they are resolved here rather than passed
|
|
97
|
+
// in by every caller. The returned object is reference stable, so it does not defeat the memo.
|
|
98
|
+
const imageServiceSettings = useImageServiceSettings();
|
|
99
|
+
|
|
59
100
|
// Prepare two image sources - a small preview image and a large main image. The idea is to
|
|
60
101
|
// display an image as soon as possible. Small images might be also available in the cache from
|
|
61
102
|
// the previous page.
|
|
62
103
|
const sources = useMemo(() => {
|
|
63
104
|
// Create a preview source when resolutions array has more than one element
|
|
64
|
-
const preview = resolutions.length > 1 ? getFullImageSource(src, resolutions[resolutions.length - 2]) : null;
|
|
105
|
+
const preview = resolutions.length > 1 ? getFullImageSource(src, resolutions[resolutions.length - 2], imageServiceSettings) : null;
|
|
65
106
|
|
|
66
107
|
// Create a main source when resolutions array has at least one element (highest resolution)
|
|
67
|
-
const main = resolutions.length > 0 ? getFullImageSource(src, resolutions[resolutions.length - 1]) : null;
|
|
108
|
+
const main = resolutions.length > 0 ? getFullImageSource(src, resolutions[resolutions.length - 1], imageServiceSettings) : null;
|
|
68
109
|
return {
|
|
69
110
|
// Only assign preview source if it is different from the main source. Image swap logic
|
|
70
111
|
// will not run when no preview source is available.
|
|
71
112
|
preview: preview !== main ? preview : null,
|
|
72
113
|
main
|
|
73
114
|
};
|
|
74
|
-
}, [resolutions, src]);
|
|
115
|
+
}, [resolutions, src, imageServiceSettings]);
|
|
75
116
|
const imgRef = useRef(null);
|
|
76
117
|
const [isInView, setIsInView] = useState(!lazy);
|
|
118
|
+
const [failedSource, setFailedSource] = useState(null);
|
|
119
|
+
|
|
120
|
+
// A failure counts against the source that produced it, not against the component. Anything that
|
|
121
|
+
// rebuilds the url - a new src, a reconfigured ratio, a different fill color - is by itself the
|
|
122
|
+
// decision to try again, so the placeholder gives way without anyone having to reset a flag.
|
|
123
|
+
//
|
|
124
|
+
// Only callers that provide a placeholder have the image taken away from them. Without one there
|
|
125
|
+
// is nothing to swap to, so a failed image stays mounted and keeps reporting through onError,
|
|
126
|
+
// exactly as it behaved before this prop existed.
|
|
127
|
+
//
|
|
128
|
+
// The null check matters: an empty resolutions array leaves no source to request, and would
|
|
129
|
+
// otherwise match the initial state and report a failure that never happened.
|
|
130
|
+
const hasFailed = sources.main !== null && failedSource === sources.main;
|
|
131
|
+
const showPlaceholder = !src || parentRendersPlaceholder || !!placeholder && hasFailed;
|
|
77
132
|
|
|
78
133
|
// Effect to create an Intersection Observer to enable lazy loading of preview images
|
|
79
134
|
useEffect(() => {
|
|
@@ -103,40 +158,41 @@ const Image = ({
|
|
|
103
158
|
|
|
104
159
|
/**
|
|
105
160
|
* Handles the onLoad event of the image.
|
|
161
|
+
* @param event The load event.
|
|
106
162
|
*/
|
|
107
|
-
const handleOnLoad = useCallback(
|
|
163
|
+
const handleOnLoad = useCallback(event => {
|
|
108
164
|
highestResolutionLoaded();
|
|
109
|
-
onLoad(
|
|
165
|
+
onLoad(event);
|
|
110
166
|
}, [highestResolutionLoaded, onLoad]);
|
|
111
167
|
|
|
112
168
|
/**
|
|
113
169
|
* Handles the onError event of the image.
|
|
170
|
+
* @param event The error event.
|
|
114
171
|
*/
|
|
115
|
-
const handleOnError = useCallback(
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
const handleOnError = useCallback(event => {
|
|
173
|
+
setFailedSource(sources.main);
|
|
174
|
+
onError(event);
|
|
175
|
+
}, [onError, sources.main]);
|
|
118
176
|
|
|
119
177
|
/**
|
|
120
178
|
* Memoized calculation of aspect ratio and CSS padding-hack ratio for responsive elements.
|
|
121
179
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* - `paddingHackRatio` {string} - The CSS padding-hack ratio as a percentage for older browsers
|
|
125
|
-
* (e.g., `56.250%` for a 16:9 ratio).
|
|
180
|
+
* aspectRatio is the ratio in the format `width / height` (e.g. `16 / 9`), paddingHackRatio the
|
|
181
|
+
* same as a percentage for older browsers (e.g. `56.250%` for a 16:9 ratio).
|
|
126
182
|
*/
|
|
127
183
|
const {
|
|
128
184
|
aspectRatio,
|
|
129
185
|
paddingHackRatio
|
|
130
186
|
} = useMemo(() => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
187
|
+
const [width, height] = ratio ?? [resolutions[resolutions.length - 1]?.width, resolutions[resolutions.length - 1]?.height];
|
|
188
|
+
|
|
189
|
+
// Fall back to a square rather than emitting "NaN / NaN" or dividing by zero. Reached when the
|
|
190
|
+
// resolutions are empty, or when a ratio arrives half typed from the admin.
|
|
191
|
+
if (!isUsableLength(width) || !isUsableLength(height)) {
|
|
192
|
+
return {
|
|
193
|
+
aspectRatio: '1 / 1',
|
|
194
|
+
paddingHackRatio: '100.000%'
|
|
195
|
+
};
|
|
140
196
|
}
|
|
141
197
|
const divisor = gcd(width, height);
|
|
142
198
|
return {
|
|
@@ -151,22 +207,24 @@ const Image = ({
|
|
|
151
207
|
background: backgroundColor,
|
|
152
208
|
paddingTop: paddingHackRatio
|
|
153
209
|
});
|
|
210
|
+
const innerStyle = {
|
|
211
|
+
aspectRatio,
|
|
212
|
+
...(isInView && sources.preview && {
|
|
213
|
+
backgroundImage: `url(${sources.preview})`,
|
|
214
|
+
backgroundSize: 'contain',
|
|
215
|
+
backgroundRepeat: 'no-repeat',
|
|
216
|
+
backgroundPosition: 'center'
|
|
217
|
+
})
|
|
218
|
+
};
|
|
154
219
|
if (unwrapped) {
|
|
155
|
-
if (
|
|
220
|
+
if (showPlaceholder) return placeholder;
|
|
156
221
|
return /*#__PURE__*/_jsx(ImageInner, {
|
|
157
222
|
ref: imgRef,
|
|
158
223
|
src: sources.main,
|
|
159
224
|
className: cx(classNameImg),
|
|
160
|
-
style:
|
|
161
|
-
aspectRatio,
|
|
162
|
-
...(isInView && sources.preview && {
|
|
163
|
-
backgroundImage: `url(${sources.preview})`,
|
|
164
|
-
backgroundSize: 'contain',
|
|
165
|
-
backgroundRepeat: 'no-repeat',
|
|
166
|
-
backgroundPosition: 'center'
|
|
167
|
-
})
|
|
168
|
-
},
|
|
225
|
+
style: innerStyle,
|
|
169
226
|
alt: alt,
|
|
227
|
+
itemProp: itemProp,
|
|
170
228
|
lazy: lazy,
|
|
171
229
|
onLoad: handleOnLoad,
|
|
172
230
|
onError: handleOnError
|
|
@@ -174,43 +232,17 @@ const Image = ({
|
|
|
174
232
|
}
|
|
175
233
|
return /*#__PURE__*/_jsx("div", {
|
|
176
234
|
className: cx(classes.container, className, 'common__image__container'),
|
|
177
|
-
children:
|
|
235
|
+
children: showPlaceholder ? placeholder : /*#__PURE__*/_jsx(ImageInner, {
|
|
178
236
|
ref: imgRef,
|
|
179
237
|
src: sources.main,
|
|
180
238
|
className: cx(classNameImg),
|
|
181
|
-
style:
|
|
182
|
-
aspectRatio,
|
|
183
|
-
...(isInView && sources.preview && {
|
|
184
|
-
backgroundImage: `url(${sources.preview})`,
|
|
185
|
-
backgroundSize: 'contain',
|
|
186
|
-
backgroundRepeat: 'no-repeat',
|
|
187
|
-
backgroundPosition: 'center'
|
|
188
|
-
})
|
|
189
|
-
},
|
|
239
|
+
style: innerStyle,
|
|
190
240
|
alt: alt,
|
|
241
|
+
itemProp: itemProp,
|
|
191
242
|
lazy: lazy,
|
|
192
243
|
onLoad: handleOnLoad,
|
|
193
244
|
onError: handleOnError
|
|
194
245
|
})
|
|
195
246
|
});
|
|
196
247
|
};
|
|
197
|
-
const defaultResolutions = [{
|
|
198
|
-
width: 440,
|
|
199
|
-
height: 440
|
|
200
|
-
}];
|
|
201
|
-
Image.defaultProps = {
|
|
202
|
-
alt: null,
|
|
203
|
-
backgroundColor: 'var(--sg-palette-background-emphasized)',
|
|
204
|
-
className: '',
|
|
205
|
-
classNameImg: '',
|
|
206
|
-
forcePlaceholder: false,
|
|
207
|
-
highestResolutionLoaded: noop,
|
|
208
|
-
onError: noop,
|
|
209
|
-
onLoad: noop,
|
|
210
|
-
ratio: null,
|
|
211
|
-
resolutions: defaultResolutions,
|
|
212
|
-
src: null,
|
|
213
|
-
unwrapped: false,
|
|
214
|
-
lazy: true
|
|
215
|
-
};
|
|
216
248
|
export default /*#__PURE__*/memo(Image);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CSSProperties, SyntheticEvent } from 'react';
|
|
2
|
+
export interface ImageInnerProps {
|
|
3
|
+
/**
|
|
4
|
+
* Alt text. Without one the image is marked decorative.
|
|
5
|
+
*/
|
|
6
|
+
alt?: string | null;
|
|
7
|
+
/**
|
|
8
|
+
* External class name for the img element.
|
|
9
|
+
*/
|
|
10
|
+
className?: string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Microdata property name, e.g. "image" for a schema.org Product. Emitted as the itemprop
|
|
13
|
+
* attribute so crawlers can pick the image out of the surrounding item.
|
|
14
|
+
*/
|
|
15
|
+
itemProp?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the browser defers loading until the image is near the viewport.
|
|
18
|
+
*/
|
|
19
|
+
lazy?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Callback that is invoked when image loading failed.
|
|
22
|
+
*/
|
|
23
|
+
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Callback that is invoked when the image has been loaded.
|
|
26
|
+
*/
|
|
27
|
+
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
|
28
|
+
/**
|
|
29
|
+
* The resolved image url.
|
|
30
|
+
*/
|
|
31
|
+
src?: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Inline styles, used to pin the aspect ratio and paint the preview behind the image.
|
|
34
|
+
*/
|
|
35
|
+
style?: CSSProperties | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The ImageInner component renders the actual image of the Image component.
|
|
39
|
+
* @param props The component props.
|
|
40
|
+
* @param ref The component reference.
|
|
41
|
+
* @returns The rendered component.
|
|
42
|
+
*/
|
|
43
|
+
declare const ImageInner: import("react").ForwardRefExoticComponent<ImageInnerProps & import("react").RefAttributes<HTMLImageElement>>;
|
|
44
|
+
export default ImageInner;
|
|
45
|
+
//# sourceMappingURL=ImageInner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageInner.d.ts","sourceRoot":"","sources":["../../../components/Image/ImageInner.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAI3D,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAC5D;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAC3D;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;OAEG;IACH,KAAK,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;CAC9B;AAeD;;;;;GAKG;AACH,QAAA,MAAM,UAAU,8GAkCd,CAAC;AAEH,eAAe,UAAU,CAAC"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import PropTypes from 'prop-types';
|
|
1
|
+
import { forwardRef } from 'react';
|
|
3
2
|
import noop from 'lodash/noop';
|
|
4
3
|
import { makeStyles } from '@shopgate/engage/styles';
|
|
5
4
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
@@ -11,25 +10,28 @@ const useStyles = makeStyles()({
|
|
|
11
10
|
width: '100%',
|
|
12
11
|
maxHeight: '100%',
|
|
13
12
|
WebkitTouchCallout: 'none',
|
|
14
|
-
fontSize: 0
|
|
13
|
+
fontSize: 0,
|
|
14
|
+
objectFit: 'contain'
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* The ImageInner component renders
|
|
20
|
-
* @param
|
|
21
|
-
* @param
|
|
22
|
-
* @returns
|
|
19
|
+
* The ImageInner component renders the actual image of the Image component.
|
|
20
|
+
* @param props The component props.
|
|
21
|
+
* @param ref The component reference.
|
|
22
|
+
* @returns The rendered component.
|
|
23
23
|
*/
|
|
24
|
-
const ImageInner = /*#__PURE__*/forwardRef(({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
const ImageInner = /*#__PURE__*/forwardRef((props, ref) => {
|
|
25
|
+
const {
|
|
26
|
+
src = null,
|
|
27
|
+
className = null,
|
|
28
|
+
alt = null,
|
|
29
|
+
itemProp,
|
|
30
|
+
lazy = false,
|
|
31
|
+
onLoad = noop,
|
|
32
|
+
onError = noop,
|
|
33
|
+
style = null
|
|
34
|
+
} = props;
|
|
33
35
|
const {
|
|
34
36
|
classes,
|
|
35
37
|
cx
|
|
@@ -37,24 +39,16 @@ const ImageInner = /*#__PURE__*/forwardRef(({
|
|
|
37
39
|
return /*#__PURE__*/_jsx("img", {
|
|
38
40
|
ref: ref,
|
|
39
41
|
loading: lazy ? 'lazy' : 'eager',
|
|
40
|
-
src: src,
|
|
42
|
+
src: src ?? undefined,
|
|
41
43
|
className: cx(classes.image, 'common__image', className),
|
|
42
|
-
alt: alt,
|
|
43
|
-
"aria-label": alt,
|
|
44
|
+
alt: alt ?? undefined,
|
|
45
|
+
"aria-label": alt ?? undefined,
|
|
44
46
|
"aria-hidden": !alt,
|
|
45
47
|
"data-test-id": "image",
|
|
48
|
+
itemProp: itemProp,
|
|
46
49
|
onLoad: onLoad,
|
|
47
50
|
onError: onError,
|
|
48
|
-
style: style
|
|
51
|
+
style: style ?? undefined
|
|
49
52
|
});
|
|
50
53
|
});
|
|
51
|
-
ImageInner.defaultProps = {
|
|
52
|
-
src: null,
|
|
53
|
-
alt: null,
|
|
54
|
-
className: null,
|
|
55
|
-
lazy: false,
|
|
56
|
-
style: null,
|
|
57
|
-
onError: noop,
|
|
58
|
-
onLoad: noop
|
|
59
|
-
};
|
|
60
54
|
export default ImageInner;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../components/Image/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -34,4 +34,11 @@ export const IS_CONNECT_EXTENSION_ATTACHED = 'IS_CONNECT_EXTENSION_ATTACHED';
|
|
|
34
34
|
*/
|
|
35
35
|
export const CONFIGURATION_COLLECTION_CREATE_EXTERNAL_IMAGE_URL = 'CONFIGURATION_COLLECTION_CREATE_EXTERNAL_IMAGE_URL';
|
|
36
36
|
export const CONFIGURATION_COLLECTION_KEY_THEME_TYPOGRAPHY = 'CONFIGURATION_COLLECTION_KEY_THEME_TYPOGRAPHY';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Capability flag the PWA sets to signal that base typography is applied on the root (`<html>`)
|
|
40
|
+
* element rather than the legacy `<body>` element. Consumers can read it to adjust behavior
|
|
41
|
+
* accordingly. Absent on older PWAs.
|
|
42
|
+
*/
|
|
43
|
+
export const CONFIGURATION_COLLECTION_KEY_HAS_ROOT_TYPOGRAPHY = 'CONFIGURATION_COLLECTION_KEY_HAS_ROOT_TYPOGRAPHY';
|
|
37
44
|
export const CONFIGURATION_COLLECTION_KEY_UNIVERSAL_LINK_HANDLER = 'universalLinkHandler';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopgate/pwa-common",
|
|
3
|
-
"version": "7.32.0-beta.
|
|
3
|
+
"version": "7.32.0-beta.6",
|
|
4
4
|
"description": "Common library for the Shopgate Connect PWA.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Shopgate <support@shopgate.com>",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@redux-devtools/extension": "^3.3.0",
|
|
19
19
|
"@sentry/browser": "6.0.1",
|
|
20
|
-
"@shopgate/pwa-benchmark": "7.32.0-beta.
|
|
20
|
+
"@shopgate/pwa-benchmark": "7.32.0-beta.6",
|
|
21
21
|
"@virtuous/conductor": "~2.5.0",
|
|
22
22
|
"@virtuous/react-conductor": "~2.5.0",
|
|
23
23
|
"@virtuous/redux-persister": "1.1.0-beta.7",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"swiper": "12.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@shopgate/pwa-core": "7.32.0-beta.
|
|
43
|
+
"@shopgate/pwa-core": "7.32.0-beta.6",
|
|
44
44
|
"@types/lodash": "^4.17.24",
|
|
45
45
|
"@types/react-portal": "^3.0.9",
|
|
46
46
|
"lodash": "^4.17.23",
|
package/providers/toast/index.js
CHANGED
|
@@ -32,47 +32,41 @@ let ToastProvider = /*#__PURE__*/function (_Component) {
|
|
|
32
32
|
if (!toast.message) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
const {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
const nextToast = {
|
|
36
|
+
id: toast.id,
|
|
37
|
+
action: toast.action,
|
|
38
|
+
actionLabel: toast.actionLabel,
|
|
39
|
+
onLongPress: toast.onLongPress,
|
|
40
|
+
message: toast.message,
|
|
41
|
+
messageParams: toast.messageParams,
|
|
42
|
+
duration: toast.duration || _this.props.theme.transitions.duration.toast
|
|
43
|
+
};
|
|
43
44
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
found.message = toast.message;
|
|
49
|
-
found.messageParams = toast.messageParams;
|
|
50
|
-
found.duration = toast.duration || _this.props.theme.transitions.duration.toast;
|
|
51
|
-
} else {
|
|
52
|
-
toasts.push({
|
|
53
|
-
id: toast.id,
|
|
54
|
-
action: toast.action,
|
|
55
|
-
actionLabel: toast.actionLabel,
|
|
56
|
-
message: toast.message,
|
|
57
|
-
messageParams: toast.messageParams,
|
|
58
|
-
duration: toast.duration || _this.props.theme.transitions.duration.toast
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
_this.setState({
|
|
45
|
+
// Update the queue immutably: consumers rely on the array reference changing to re-render
|
|
46
|
+
// (e.g. the SnackBar memoizes the currently shown toast on this reference). A toast whose id
|
|
47
|
+
// is already queued replaces that entry with the new data; otherwise it is appended.
|
|
48
|
+
_this.setState(({
|
|
62
49
|
toasts
|
|
50
|
+
}) => {
|
|
51
|
+
const exists = toasts.some(({
|
|
52
|
+
id
|
|
53
|
+
}) => id === toast.id);
|
|
54
|
+
return {
|
|
55
|
+
toasts: exists ? toasts.map(item => item.id === toast.id ? nextToast : item) : [].concat(toasts, [nextToast])
|
|
56
|
+
};
|
|
63
57
|
});
|
|
64
58
|
};
|
|
65
59
|
/**
|
|
66
60
|
* Removes the first toast from the list.
|
|
67
61
|
*/
|
|
68
62
|
_this.removeToast = () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
toasts.shift();
|
|
73
|
-
_this.setState({
|
|
63
|
+
// Drop the first toast immutably so the array reference changes and the SnackBar advances to
|
|
64
|
+
// the next queued toast (rather than re-rendering the same, now-stale, memoized toast).
|
|
65
|
+
_this.setState(({
|
|
74
66
|
toasts
|
|
75
|
-
})
|
|
67
|
+
}) => ({
|
|
68
|
+
toasts: toasts.slice(1)
|
|
69
|
+
}));
|
|
76
70
|
};
|
|
77
71
|
_this.flushToasts = () => {
|
|
78
72
|
if (_this.state.toasts.length) {
|
|
@@ -90,11 +84,21 @@ let ToastProvider = /*#__PURE__*/function (_Component) {
|
|
|
90
84
|
}
|
|
91
85
|
|
|
92
86
|
/**
|
|
93
|
-
*
|
|
94
|
-
* @returns {Object}
|
|
87
|
+
* Removes the UIEvents listeners registered in the constructor.
|
|
95
88
|
*/
|
|
96
89
|
_inheritsLoose(ToastProvider, _Component);
|
|
97
90
|
var _proto = ToastProvider.prototype;
|
|
91
|
+
_proto.componentWillUnmount = function componentWillUnmount() {
|
|
92
|
+
UIEvents.removeListener(this.constructor.ADD, this.addToast);
|
|
93
|
+
UIEvents.removeListener(this.constructor.FLUSH, this.flushToasts);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns the context value to be passed to consumers. The object reference is cached and only
|
|
98
|
+
* rebuilt when `toasts` changes, so consumers (Toaster → SnackBarContainer → SnackBar) don't
|
|
99
|
+
* re-render on every unrelated ToastProvider render.
|
|
100
|
+
* @returns {Object}
|
|
101
|
+
*/;
|
|
98
102
|
/**
|
|
99
103
|
* @returns {JSX}
|
|
100
104
|
*/
|
|
@@ -107,14 +111,24 @@ let ToastProvider = /*#__PURE__*/function (_Component) {
|
|
|
107
111
|
return _createClass(ToastProvider, [{
|
|
108
112
|
key: "provided",
|
|
109
113
|
get: function () {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
if (!this.providedCache || this.providedCache.toasts !== this.state.toasts) {
|
|
115
|
+
this.providedCache = {
|
|
116
|
+
addToast: this.addToast,
|
|
117
|
+
removeToast: this.removeToast,
|
|
118
|
+
toasts: this.state.toasts
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return this.providedCache;
|
|
115
122
|
}
|
|
116
123
|
}]);
|
|
117
124
|
}(Component);
|
|
118
125
|
ToastProvider.ADD = 'toast_add';
|
|
119
126
|
ToastProvider.FLUSH = 'toast_flush';
|
|
120
|
-
|
|
127
|
+
const ThemedToastProvider = withTheme(ToastProvider);
|
|
128
|
+
|
|
129
|
+
// The HOC returns a new component which doesn't carry over the statics of the wrapped class. They
|
|
130
|
+
// are part of the public API of this module (consumers emit UIEvents on ToastProvider.ADD), so they
|
|
131
|
+
// are re-attached explicitly.
|
|
132
|
+
ThemedToastProvider.ADD = ToastProvider.ADD;
|
|
133
|
+
ThemedToastProvider.FLUSH = ToastProvider.FLUSH;
|
|
134
|
+
export default ThemedToastProvider;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { render, act } from '@testing-library/react';
|
|
3
|
+
import ToastProvider from "./index";
|
|
4
|
+
import ToastContext from "./context";
|
|
5
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
+
jest.mock('@shopgate/pwa-core', () => ({
|
|
7
|
+
UIEvents: {
|
|
8
|
+
addListener: jest.fn(),
|
|
9
|
+
removeListener: jest.fn(),
|
|
10
|
+
emit: jest.fn()
|
|
11
|
+
}
|
|
12
|
+
}));
|
|
13
|
+
jest.mock('@shopgate/pwa-common/helpers/config', () => ({
|
|
14
|
+
themeConfig: {
|
|
15
|
+
variables: {
|
|
16
|
+
toast: {
|
|
17
|
+
duration: 5000
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}));
|
|
22
|
+
let ctx;
|
|
23
|
+
const Capture = () => {
|
|
24
|
+
ctx = useContext(ToastContext);
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
const renderProvider = () => render(/*#__PURE__*/_jsx(ToastProvider, {
|
|
28
|
+
children: /*#__PURE__*/_jsx(Capture, {})
|
|
29
|
+
}));
|
|
30
|
+
const toast = (id, message) => ({
|
|
31
|
+
id,
|
|
32
|
+
message
|
|
33
|
+
});
|
|
34
|
+
describe('ToastProvider', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
ctx = undefined;
|
|
37
|
+
});
|
|
38
|
+
it('exposes the event name statics on the exported component', () => {
|
|
39
|
+
// They survive the withTheme() wrapping - consumers emit UIEvents on them.
|
|
40
|
+
expect(ToastProvider.ADD).toBe('toast_add');
|
|
41
|
+
expect(ToastProvider.FLUSH).toBe('toast_flush');
|
|
42
|
+
});
|
|
43
|
+
it('appends a toast and applies the default duration', () => {
|
|
44
|
+
renderProvider();
|
|
45
|
+
act(() => {
|
|
46
|
+
ctx.addToast(toast('a', 'A'));
|
|
47
|
+
});
|
|
48
|
+
expect(ctx.toasts).toHaveLength(1);
|
|
49
|
+
expect(ctx.toasts[0]).toMatchObject({
|
|
50
|
+
id: 'a',
|
|
51
|
+
message: 'A',
|
|
52
|
+
duration: 5000
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('ignores a toast without a message', () => {
|
|
56
|
+
renderProvider();
|
|
57
|
+
act(() => {
|
|
58
|
+
ctx.addToast({
|
|
59
|
+
id: 'x'
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
expect(ctx.toasts).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
it('replaces a same-id toast in place, keeping its length and position', () => {
|
|
65
|
+
renderProvider();
|
|
66
|
+
act(() => {
|
|
67
|
+
ctx.addToast(toast('a', 'A'));
|
|
68
|
+
});
|
|
69
|
+
act(() => {
|
|
70
|
+
ctx.addToast(toast('b', 'B'));
|
|
71
|
+
});
|
|
72
|
+
const before = ctx.toasts;
|
|
73
|
+
act(() => {
|
|
74
|
+
ctx.addToast(toast('a', 'A2'));
|
|
75
|
+
});
|
|
76
|
+
expect(ctx.toasts).toHaveLength(2);
|
|
77
|
+
expect(ctx.toasts[0]).toMatchObject(toast('a', 'A2')); // content updated
|
|
78
|
+
expect(ctx.toasts[1]).toMatchObject(toast('b', 'B')); // sibling untouched
|
|
79
|
+
expect(ctx.toasts).not.toBe(before); // new array reference (immutable update)
|
|
80
|
+
expect(ctx.toasts[0]).not.toBe(before[0]); // new head object
|
|
81
|
+
});
|
|
82
|
+
it('removes the first toast (FIFO) on removeToast', () => {
|
|
83
|
+
renderProvider();
|
|
84
|
+
act(() => {
|
|
85
|
+
ctx.addToast(toast('a', 'A'));
|
|
86
|
+
});
|
|
87
|
+
act(() => {
|
|
88
|
+
ctx.addToast(toast('b', 'B'));
|
|
89
|
+
});
|
|
90
|
+
act(() => {
|
|
91
|
+
ctx.removeToast();
|
|
92
|
+
});
|
|
93
|
+
expect(ctx.toasts).toHaveLength(1);
|
|
94
|
+
expect(ctx.toasts[0]).toMatchObject(toast('b', 'B'));
|
|
95
|
+
});
|
|
96
|
+
it('keeps the context value referentially stable when toasts do not change', () => {
|
|
97
|
+
const {
|
|
98
|
+
rerender
|
|
99
|
+
} = renderProvider();
|
|
100
|
+
act(() => {
|
|
101
|
+
ctx.addToast(toast('a', 'A'));
|
|
102
|
+
});
|
|
103
|
+
const provided = ctx;
|
|
104
|
+
// Re-render the provider without changing the queue.
|
|
105
|
+
rerender(/*#__PURE__*/_jsx(ToastProvider, {
|
|
106
|
+
children: /*#__PURE__*/_jsx(Capture, {})
|
|
107
|
+
}));
|
|
108
|
+
expect(ctx).toBe(provided); // same context object → consumers don't needlessly re-render
|
|
109
|
+
});
|
|
110
|
+
});
|
package/subscriptions/error.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import "core-js/modules/es.array.includes.js";
|
|
2
|
-
import after from 'lodash/after';
|
|
3
|
-
import before from 'lodash/before';
|
|
4
|
-
import over from 'lodash/over';
|
|
5
2
|
import { isAvailable } from '@shopgate/native-modules';
|
|
6
3
|
import { init, addBreadcrumb, configureScope, captureException, captureMessage, captureEvent, withScope, Severity as SentrySeverity } from '@sentry/browser';
|
|
7
4
|
import { EBIGAPI, emitter, errorManager, ETIMEOUT, ENETUNREACH, EUNKNOWN, EFAVORITE } from '@shopgate/pwa-core';
|
|
@@ -12,7 +9,7 @@ import {
|
|
|
12
9
|
// eslint-disable-next-line import/no-named-default
|
|
13
10
|
default as appConfig, themeName, pckVersion } from "../helpers/config";
|
|
14
11
|
import { env } from "../helpers/environment";
|
|
15
|
-
import { transformGeneralPipelineError } from "./helpers/pipeline";
|
|
12
|
+
import { transformGeneralPipelineError, getDisplayErrorMessage } from "./helpers/pipeline";
|
|
16
13
|
import { historyPop } from "../actions/router";
|
|
17
14
|
import showModal from "../actions/modal/showModal";
|
|
18
15
|
import { getUserData } from "../selectors/user";
|
|
@@ -24,6 +21,9 @@ import { getRouterStack } from "../selectors/router";
|
|
|
24
21
|
import { MODAL_PIPELINE_ERROR } from "../constants/ModalTypes";
|
|
25
22
|
import ToastProvider from "../providers/toast";
|
|
26
23
|
|
|
24
|
+
// Generic, translated fallback shown when a backend error carries no code we can map to a message.
|
|
25
|
+
const GENERIC_ERROR_MESSAGE = 'modal.body_error';
|
|
26
|
+
|
|
27
27
|
/**
|
|
28
28
|
* App errors subscriptions.
|
|
29
29
|
* @param {Function} subscribe The subscribe function.
|
|
@@ -60,15 +60,35 @@ export default subscribe => {
|
|
|
60
60
|
error
|
|
61
61
|
} = action;
|
|
62
62
|
const {
|
|
63
|
-
message,
|
|
64
63
|
code,
|
|
65
64
|
context,
|
|
66
65
|
meta = {}
|
|
67
66
|
} = error;
|
|
68
67
|
const {
|
|
69
|
-
behavior
|
|
68
|
+
behavior,
|
|
69
|
+
message: originalMessage
|
|
70
70
|
} = meta;
|
|
71
|
-
|
|
71
|
+
|
|
72
|
+
// Never surface a raw backend message to the user: show the extension's own translated
|
|
73
|
+
// message, a code-mapped translated message, or a generic fallback. The resolver is the single
|
|
74
|
+
// source of truth for both the text and whether it is ready-to-display (`displayTranslated`) or
|
|
75
|
+
// a locale key that must still go through I18n.Text.
|
|
76
|
+
const {
|
|
77
|
+
message: displayMessage,
|
|
78
|
+
translated: displayTranslated
|
|
79
|
+
} = getDisplayErrorMessage(error, GENERIC_ERROR_MESSAGE);
|
|
80
|
+
|
|
81
|
+
// Unknown/generic and connection-style backend errors are treated as general platform issues.
|
|
82
|
+
// EUNKNOWN is always treated as general — even when the backend supplied a translated message —
|
|
83
|
+
// so it is shown as a toast with its translated message or generic fallback, never a modal.
|
|
84
|
+
const isConnectionError = displayMessage === 'error.general' || [ETIMEOUT, ENETUNREACH].includes(code);
|
|
85
|
+
const isGeneralError = isConnectionError || displayMessage === GENERIC_ERROR_MESSAGE || code === EUNKNOWN;
|
|
86
|
+
|
|
87
|
+
// Some pipeline actions (e.g. fetchCategory) register an error behavior for *every* error via
|
|
88
|
+
// `setResponseBehavior`. Those behaviors are only meaningful for the specific errors they
|
|
89
|
+
// expect; when the platform fails with an unexpected/unmappable message we skip the behavior
|
|
90
|
+
// and fall through to the generic toast handling instead of, for example, showing a modal.
|
|
91
|
+
if (behavior && !isGeneralError) {
|
|
72
92
|
behavior({
|
|
73
93
|
dispatch,
|
|
74
94
|
getState,
|
|
@@ -78,38 +98,41 @@ export default subscribe => {
|
|
|
78
98
|
return;
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Shows the pipeline error modal. When openWithDetails is set, the modal opens directly on the
|
|
103
|
+
* developer detail view (pipeline, code, raw message, params) — used for the toast long-press.
|
|
104
|
+
* @param {boolean} [openWithDetails=false] Whether to open the modal in developer detail mode.
|
|
105
|
+
*/
|
|
106
|
+
const showModalError = (openWithDetails = false) => {
|
|
83
107
|
dispatch(showModal({
|
|
84
108
|
confirm: 'modal.ok',
|
|
85
109
|
dismiss: null,
|
|
86
110
|
title: null,
|
|
87
|
-
message,
|
|
111
|
+
message: displayMessage,
|
|
88
112
|
type: MODAL_PIPELINE_ERROR,
|
|
89
113
|
params: {
|
|
90
114
|
pipeline: context,
|
|
91
115
|
request: meta.input,
|
|
92
|
-
message:
|
|
93
|
-
code
|
|
116
|
+
message: originalMessage,
|
|
117
|
+
code,
|
|
118
|
+
translated: displayTranslated,
|
|
119
|
+
messageParams: meta.additionalParams,
|
|
120
|
+
openWithDetails
|
|
94
121
|
}
|
|
95
122
|
}));
|
|
96
123
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
message: 'error.general',
|
|
109
|
-
action: over([showToast, showToastAfter])
|
|
110
|
-
});
|
|
124
|
+
|
|
125
|
+
// General platform errors surface as a toast instead of a modal.
|
|
126
|
+
// Long-pressing the toast opens the error modal in developer detail mode.
|
|
127
|
+
if (isGeneralError) {
|
|
128
|
+
// Connection-style errors show the generic connection message, but never override a message
|
|
129
|
+
// an extension already translated for us.
|
|
130
|
+
const useGenericConnectionText = isConnectionError && !displayTranslated;
|
|
131
|
+
events.emit(ToastProvider.ADD, {
|
|
132
|
+
id: 'pipeline.error',
|
|
133
|
+
message: useGenericConnectionText ? 'error.general' : displayMessage,
|
|
134
|
+
onLongPress: () => showModalError(true)
|
|
111
135
|
});
|
|
112
|
-
showToast();
|
|
113
136
|
return;
|
|
114
137
|
}
|
|
115
138
|
showModalError();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { i18n } from '@shopgate/engage/core/helpers';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @param {Object} error error
|
|
3
5
|
* @returns {string|*}
|
|
@@ -12,4 +14,70 @@ export function transformGeneralPipelineError(error) {
|
|
|
12
14
|
return 'error.general';
|
|
13
15
|
}
|
|
14
16
|
return message;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} DisplayErrorMessage
|
|
21
|
+
* @property {string} message The text to display — an already-translated string or a locale key.
|
|
22
|
+
* @property {boolean} translated Whether `message` is ready-to-display text (`true`) or a locale
|
|
23
|
+
* key that still needs to go through I18n.Text (`false`).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the message to display for a pipeline error, without ever surfacing raw backend text.
|
|
28
|
+
* Also reports whether the result is ready-to-display text or a locale key
|
|
29
|
+
* Precedence:
|
|
30
|
+
* 1. An extension's own message when it is explicitly flagged as already translated.
|
|
31
|
+
* 2. A resolvable i18n key — safe to surface even when errorManager did not remap it, because a
|
|
32
|
+
* translation key (e.g. `cart.error_out_of_stock`) is not raw backend text. `i18n.has` confirms
|
|
33
|
+
* the key actually resolves in the loaded locales, rather than merely looking like a key.
|
|
34
|
+
* 3. The code-mapped message. When errorManager maps the error code it resolves a message that
|
|
35
|
+
* differs from the raw backend text (`error.meta.message`).
|
|
36
|
+
* 4. A generic, translated fallback otherwise — including when nothing mapped and errorManager fell
|
|
37
|
+
* back to the raw backend message (`error.message === error.meta.message`).
|
|
38
|
+
* @param {Object} error The queued pipeline error.
|
|
39
|
+
* @param {string} error.message The message resolved by errorManager (code-mapped or raw fallback).
|
|
40
|
+
* @param {Object} [error.meta] The error meta data.
|
|
41
|
+
* @param {boolean} [error.meta.translated] Whether the backend message is already translated.
|
|
42
|
+
* @param {string} [error.meta.message] The original, raw backend message.
|
|
43
|
+
* @param {string} genericMessage The generic fallback message key.
|
|
44
|
+
* @returns {DisplayErrorMessage}
|
|
45
|
+
*/
|
|
46
|
+
export function getDisplayErrorMessage(error, genericMessage) {
|
|
47
|
+
const {
|
|
48
|
+
message,
|
|
49
|
+
meta = {}
|
|
50
|
+
} = error;
|
|
51
|
+
const {
|
|
52
|
+
translated,
|
|
53
|
+
message: originalMessage
|
|
54
|
+
} = meta;
|
|
55
|
+
if (translated && originalMessage) {
|
|
56
|
+
return {
|
|
57
|
+
message: originalMessage,
|
|
58
|
+
translated: true
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A translation key is never raw backend text, so surface it even when errorManager left it
|
|
63
|
+
// untouched (`message === originalMessage`).
|
|
64
|
+
if (typeof message === 'string' && i18n.has(message)) {
|
|
65
|
+
return {
|
|
66
|
+
message,
|
|
67
|
+
translated: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Only trust `message` when we have the raw backend message to compare against and a mapping
|
|
72
|
+
// actually replaced it. Without the raw message we cannot rule out that `message` is raw text.
|
|
73
|
+
if (originalMessage !== undefined && message !== originalMessage) {
|
|
74
|
+
return {
|
|
75
|
+
message,
|
|
76
|
+
translated: false
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
message: genericMessage,
|
|
81
|
+
translated: false
|
|
82
|
+
};
|
|
15
83
|
}
|
package/styles/reset/form.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated used @shopgate/engage/styles instead
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { injectGlobal } from '@shopgate/engage/styles';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 1. Correct the inability to style clickable types in iOS and Safari.
|
|
9
|
-
* since iOS 15 button has a default color of blue rgb(0, 122, 255);
|
|
10
|
-
*
|
|
11
|
-
* 1. Change font properties to `inherit` in Safari.
|
|
12
|
-
* 2. Correct the inability to style clickable types in iOS and Safari.
|
|
13
|
-
*
|
|
14
|
-
* Remove the inner padding and cancel buttons in Chrome and Safari for OS X.
|
|
15
|
-
*/
|
|
16
|
-
injectGlobal({
|
|
17
|
-
'button, html [type="button"], [type="reset"], [type="submit"]': {
|
|
18
|
-
WebkitAppearance: 'button'
|
|
19
|
-
},
|
|
20
|
-
button: {
|
|
21
|
-
color: 'inherit',
|
|
22
|
-
borderRadius: 0
|
|
23
|
-
},
|
|
24
|
-
'::-webkit-file-upload-button': {
|
|
25
|
-
font: 'inherit',
|
|
26
|
-
WebkitAppearance: 'button'
|
|
27
|
-
},
|
|
28
|
-
'::-webkit-search-cancel-button, ::-webkit-search-decoration': {
|
|
29
|
-
WebkitAppearance: 'none'
|
|
30
|
-
},
|
|
31
|
-
'button, input, select, textarea': {
|
|
32
|
-
background: 'transparent',
|
|
33
|
-
border: 0,
|
|
34
|
-
fontSize: '1em',
|
|
35
|
-
margin: 0
|
|
36
|
-
},
|
|
37
|
-
'input, select, textarea': {
|
|
38
|
-
fontFamily: 'inherit'
|
|
39
|
-
},
|
|
40
|
-
'button, textarea': {
|
|
41
|
-
fontFamily: 'inherit',
|
|
42
|
-
lineHeight: 'inherit'
|
|
43
|
-
},
|
|
44
|
-
'button, label, [type="checkbox"], [type="radio"]': {
|
|
45
|
-
cursor: 'pointer'
|
|
46
|
-
},
|
|
47
|
-
label: {
|
|
48
|
-
display: 'inline-block'
|
|
49
|
-
},
|
|
50
|
-
textarea: {
|
|
51
|
-
minHeight: '5em',
|
|
52
|
-
maxWidth: '100%',
|
|
53
|
-
resize: 'none'
|
|
54
|
-
},
|
|
55
|
-
'[type="search"]': {
|
|
56
|
-
WebkitAppearance: 'textfield'
|
|
57
|
-
}
|
|
58
|
-
});
|
package/styles/reset/index.js
DELETED
package/styles/reset/media.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated used @shopgate/engage/styles/reset instead
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { injectGlobal } from '@shopgate/engage/styles';
|
|
6
|
-
injectGlobal({
|
|
7
|
-
'audio:not([controls])': {
|
|
8
|
-
display: 'none',
|
|
9
|
-
height: 0
|
|
10
|
-
},
|
|
11
|
-
figure: {
|
|
12
|
-
margin: 0
|
|
13
|
-
},
|
|
14
|
-
iframe: {
|
|
15
|
-
border: 0,
|
|
16
|
-
display: 'block',
|
|
17
|
-
width: '100%'
|
|
18
|
-
},
|
|
19
|
-
'img, svg': {
|
|
20
|
-
display: 'block',
|
|
21
|
-
maxWidth: '100%'
|
|
22
|
-
},
|
|
23
|
-
progress: {
|
|
24
|
-
verticalAlign: 'baseline'
|
|
25
|
-
}
|
|
26
|
-
});
|
package/styles/reset/root.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated used @shopgate/engage/styles instead
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { injectGlobal } from '@shopgate/engage/styles';
|
|
6
|
-
import { themeConfig } from "../../helpers/config";
|
|
7
|
-
const {
|
|
8
|
-
typography
|
|
9
|
-
} = themeConfig;
|
|
10
|
-
injectGlobal({
|
|
11
|
-
'*, *:before, *:after': {
|
|
12
|
-
boxSizing: 'border-box'
|
|
13
|
-
},
|
|
14
|
-
'html, body': {
|
|
15
|
-
WebkitTapHighlightColor: 'transparent',
|
|
16
|
-
width: '100%',
|
|
17
|
-
height: '100%'
|
|
18
|
-
},
|
|
19
|
-
html: {
|
|
20
|
-
overflow: 'hidden',
|
|
21
|
-
MozOsxFontSmoothing: 'grayscale',
|
|
22
|
-
WebkitFontSmoothing: 'antialiased',
|
|
23
|
-
MsTextSizeAdjust: '100%',
|
|
24
|
-
WebkitTextSizeAdjust: '100%',
|
|
25
|
-
minHeight: '100%'
|
|
26
|
-
},
|
|
27
|
-
body: {
|
|
28
|
-
font: `${typography.rootSize}px/${typography.lineHeight} ${typography.family}`,
|
|
29
|
-
overflow: 'auto',
|
|
30
|
-
margin: 0,
|
|
31
|
-
WebkitOverflowScrolling: 'touch',
|
|
32
|
-
WebkitUserSelect: 'none'
|
|
33
|
-
},
|
|
34
|
-
'[data-pattern]': {
|
|
35
|
-
height: '100% !important'
|
|
36
|
-
}
|
|
37
|
-
});
|
package/styles/reset/table.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated used @shopgate/engage/styles instead
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { injectGlobal } from '@shopgate/engage/styles';
|
|
6
|
-
injectGlobal({
|
|
7
|
-
table: {
|
|
8
|
-
borderCollapse: 'collapse',
|
|
9
|
-
minWidth: '100%'
|
|
10
|
-
},
|
|
11
|
-
'td, th': {
|
|
12
|
-
verticalAlign: 'top'
|
|
13
|
-
}
|
|
14
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated used @shopgate/engage/styles instead
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { injectGlobal } from '@shopgate/engage/styles';
|
|
6
|
-
import { rem } from "../../helpers/style";
|
|
7
|
-
injectGlobal({
|
|
8
|
-
a: {
|
|
9
|
-
color: 'inherit',
|
|
10
|
-
textDecoration: 'none',
|
|
11
|
-
WebkitTextDecorationSkip: 'objects'
|
|
12
|
-
},
|
|
13
|
-
'a:hover, a:focus, a:active, [tabindex]': {
|
|
14
|
-
outline: 0
|
|
15
|
-
},
|
|
16
|
-
'ol, ul': {
|
|
17
|
-
listStyle: 'none',
|
|
18
|
-
margin: 0,
|
|
19
|
-
paddingLeft: 0
|
|
20
|
-
},
|
|
21
|
-
'b, strong': {
|
|
22
|
-
fontWeight: 700
|
|
23
|
-
},
|
|
24
|
-
small: {
|
|
25
|
-
fontSize: rem(13)
|
|
26
|
-
},
|
|
27
|
-
'sub, sup': {
|
|
28
|
-
lineHeight: 0
|
|
29
|
-
}
|
|
30
|
-
});
|