cozy-ui-plus 12.1.5 → 12.2.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/CHANGELOG.md +10 -0
- package/dist/Intent/IntentDialogOpener/index.js +8 -2
- package/dist/Intent/IntentIframe/index.js +53 -28
- package/dist/src/Intent/IntentDialogOpener/index.d.ts +3 -0
- package/dist/src/Intent/IntentDialogOpener/index.spec.d.ts +1 -0
- package/dist/src/Intent/IntentIframe/index.spec.d.ts +1 -0
- package/dist/stylesheet.css +21 -1
- package/package.json +4 -4
- package/src/Intent/IntentDialogOpener/Readme.md +5 -0
- package/src/Intent/IntentDialogOpener/index.jsx +6 -1
- package/src/Intent/IntentDialogOpener/index.spec.jsx +44 -0
- package/src/Intent/IntentIframe/Readme.md +5 -0
- package/src/Intent/IntentIframe/index.jsx +35 -12
- package/src/Intent/IntentIframe/index.spec.jsx +121 -0
- package/src/Intent/IntentIframe/styles.styl +21 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [12.2.1](https://github.com/cozy/cozy-libs/compare/cozy-ui-plus@12.2.0...cozy-ui-plus@12.2.1) (2026-07-28)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package cozy-ui-plus
|
|
9
|
+
|
|
10
|
+
# [12.2.0](https://github.com/cozy/cozy-libs/compare/cozy-ui-plus@12.1.5...cozy-ui-plus@12.2.0) (2026-07-27)
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **intent:** Wait for intent readiness signal ([b8d64dd](https://github.com/cozy/cozy-libs/commit/b8d64dd023e57ef976df08fda295ab6fa643b7ad))
|
|
15
|
+
|
|
6
16
|
## [12.1.5](https://github.com/cozy/cozy-libs/compare/cozy-ui-plus@12.1.4...cozy-ui-plus@12.1.5) (2026-07-23)
|
|
7
17
|
|
|
8
18
|
**Note:** Version bump only for package cozy-ui-plus
|
|
@@ -23,7 +23,7 @@ var _Dialog = _interopRequireDefault(require("cozy-ui/transpiled/react/Dialog"))
|
|
|
23
23
|
|
|
24
24
|
var _IntentIframe = _interopRequireWildcard(require("../IntentIframe"));
|
|
25
25
|
|
|
26
|
-
var _excluded = ["options", "action", "doctype", "children", "closable", "showCloseButton", "create", "tag", "onComplete", "onDismiss", "onReadyToUse", "iframeProps", "Component"];
|
|
26
|
+
var _excluded = ["options", "action", "doctype", "children", "closable", "showCloseButton", "create", "tag", "onComplete", "onDismiss", "onReadyToUse", "waitForReadyToUse", "iframeProps", "Component"];
|
|
27
27
|
|
|
28
28
|
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
29
29
|
|
|
@@ -45,6 +45,7 @@ var IntentDialogOpener = function IntentDialogOpener(props) {
|
|
|
45
45
|
onComplete = props.onComplete,
|
|
46
46
|
onDismiss = props.onDismiss,
|
|
47
47
|
onReadyToUse = props.onReadyToUse,
|
|
48
|
+
waitForReadyToUse = props.waitForReadyToUse,
|
|
48
49
|
iframeProps = props.iframeProps,
|
|
49
50
|
Component = props.Component,
|
|
50
51
|
componentProps = (0, _objectWithoutProperties2.default)(props, _excluded);
|
|
@@ -96,6 +97,7 @@ var IntentDialogOpener = function IntentDialogOpener(props) {
|
|
|
96
97
|
onCancel: handleDismiss,
|
|
97
98
|
onTerminate: handleComplete,
|
|
98
99
|
onReadyToUse: onReadyToUse,
|
|
100
|
+
waitForReadyToUse: waitForReadyToUse,
|
|
99
101
|
iframeProps: iframeProps
|
|
100
102
|
})));
|
|
101
103
|
}
|
|
@@ -116,6 +118,9 @@ IntentDialogOpener.propTypes = {
|
|
|
116
118
|
/** Called when the intent service signals it is ready to use */
|
|
117
119
|
onReadyToUse: _propTypes.default.func,
|
|
118
120
|
|
|
121
|
+
/** Keep the loading state until the intent service signals it is ready to use */
|
|
122
|
+
waitForReadyToUse: _propTypes.default.bool,
|
|
123
|
+
|
|
119
124
|
/** Action you want to execute */
|
|
120
125
|
action: _propTypes.default.string.isRequired,
|
|
121
126
|
|
|
@@ -141,7 +146,8 @@ IntentDialogOpener.defaultProps = {
|
|
|
141
146
|
tag: 'span',
|
|
142
147
|
closable: true,
|
|
143
148
|
Component: _Dialog.default,
|
|
144
|
-
showCloseButton: true
|
|
149
|
+
showCloseButton: true,
|
|
150
|
+
waitForReadyToUse: false
|
|
145
151
|
};
|
|
146
152
|
var _default = IntentDialogOpener;
|
|
147
153
|
exports.default = _default;
|
|
@@ -46,6 +46,7 @@ function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Re
|
|
|
46
46
|
var styles = {
|
|
47
47
|
"intentContainer": "styles__intentContainer___5Zqqq",
|
|
48
48
|
"intentPlaceHolder": "styles__intentPlaceHolder___1f_u6",
|
|
49
|
+
"intentContainer__loader": "styles__intentContainer__loader___1krAq",
|
|
49
50
|
"intentContainer__error": "styles__intentContainer__error___YO3Lz"
|
|
50
51
|
};
|
|
51
52
|
var DEFAULT_DATA = {
|
|
@@ -71,16 +72,36 @@ var IntentIframe = /*#__PURE__*/function (_React$Component) {
|
|
|
71
72
|
|
|
72
73
|
_this = _super.call.apply(_super, [this].concat(args));
|
|
73
74
|
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "state", {
|
|
74
|
-
|
|
75
|
+
error: null,
|
|
76
|
+
frameLoaded: false,
|
|
77
|
+
readyToUse: false
|
|
75
78
|
});
|
|
76
79
|
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "onFrameLoaded", function () {
|
|
77
|
-
|
|
80
|
+
_this.setState({
|
|
81
|
+
frameLoaded: true
|
|
82
|
+
}, function () {
|
|
83
|
+
if (!_this.props.waitForReadyToUse || _this.state.readyToUse) {
|
|
84
|
+
_this.setIsLoading(false);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "onReadyToUse", function () {
|
|
89
|
+
var _this$props$onReadyTo, _this$props;
|
|
78
90
|
|
|
79
91
|
_this.setState({
|
|
80
|
-
|
|
92
|
+
readyToUse: true
|
|
93
|
+
}, function () {
|
|
94
|
+
if (_this.props.waitForReadyToUse && _this.state.frameLoaded) {
|
|
95
|
+
_this.setIsLoading(false);
|
|
96
|
+
}
|
|
81
97
|
});
|
|
82
98
|
|
|
83
|
-
(_this$props$
|
|
99
|
+
(_this$props$onReadyTo = (_this$props = _this.props).onReadyToUse) === null || _this$props$onReadyTo === void 0 ? void 0 : _this$props$onReadyTo.call(_this$props);
|
|
100
|
+
});
|
|
101
|
+
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "setIsLoading", function (isLoading) {
|
|
102
|
+
var _this$props$iframePro, _this$props$iframePro2;
|
|
103
|
+
|
|
104
|
+
(_this$props$iframePro = _this.props.iframeProps) === null || _this$props$iframePro === void 0 ? void 0 : (_this$props$iframePro2 = _this$props$iframePro.setIsLoading) === null || _this$props$iframePro2 === void 0 ? void 0 : _this$props$iframePro2.call(_this$props$iframePro, isLoading);
|
|
84
105
|
});
|
|
85
106
|
return _this;
|
|
86
107
|
}
|
|
@@ -90,14 +111,14 @@ var IntentIframe = /*#__PURE__*/function (_React$Component) {
|
|
|
90
111
|
value: function componentDidMount() {
|
|
91
112
|
var _this2 = this;
|
|
92
113
|
|
|
93
|
-
var _this$
|
|
94
|
-
action = _this$
|
|
95
|
-
data = _this$
|
|
96
|
-
type = _this$
|
|
97
|
-
onCancel = _this$
|
|
98
|
-
onError = _this$
|
|
99
|
-
onTerminate = _this$
|
|
100
|
-
client = _this$
|
|
114
|
+
var _this$props2 = this.props,
|
|
115
|
+
action = _this$props2.action,
|
|
116
|
+
data = _this$props2.data,
|
|
117
|
+
type = _this$props2.type,
|
|
118
|
+
onCancel = _this$props2.onCancel,
|
|
119
|
+
onError = _this$props2.onError,
|
|
120
|
+
onTerminate = _this$props2.onTerminate,
|
|
121
|
+
client = _this$props2.client;
|
|
101
122
|
console.warn('Be carful to use `withBreakpoints()` and not `useBreakpoints()` in intents. See https://github.com/cozy/cozy-ui/issues/1807');
|
|
102
123
|
var create;
|
|
103
124
|
|
|
@@ -110,28 +131,22 @@ var IntentIframe = /*#__PURE__*/function (_React$Component) {
|
|
|
110
131
|
create = intents.create;
|
|
111
132
|
}
|
|
112
133
|
|
|
113
|
-
this.setState({
|
|
114
|
-
loading: true
|
|
115
|
-
});
|
|
116
134
|
create(action, type, _objectSpread(_objectSpread({}, DEFAULT_DATA), data)).start(this.intentViewer, {
|
|
117
135
|
onReady: this.onFrameLoaded,
|
|
118
136
|
onHideCross: this.props.onHideCross,
|
|
119
137
|
onShowCross: this.props.onShowCross,
|
|
120
|
-
onReadyToUse: this.
|
|
138
|
+
onReadyToUse: this.onReadyToUse
|
|
121
139
|
}).then(function (result) {
|
|
122
140
|
// eslint-disable-next-line promise/always-return
|
|
123
141
|
result ? onTerminate && onTerminate(result) : onCancel();
|
|
124
142
|
}).catch(function (error) {
|
|
125
|
-
var _this2$props$iframePr, _this2$props$iframePr2;
|
|
126
|
-
|
|
127
143
|
onError === null || onError === void 0 ? void 0 : onError(error);
|
|
128
144
|
|
|
129
145
|
_this2.setState({
|
|
130
|
-
error: error
|
|
131
|
-
loading: false
|
|
146
|
+
error: error
|
|
132
147
|
});
|
|
133
148
|
|
|
134
|
-
|
|
149
|
+
_this2.setIsLoading(false);
|
|
135
150
|
});
|
|
136
151
|
}
|
|
137
152
|
}, {
|
|
@@ -139,19 +154,27 @@ var IntentIframe = /*#__PURE__*/function (_React$Component) {
|
|
|
139
154
|
value: function render() {
|
|
140
155
|
var _this3 = this;
|
|
141
156
|
|
|
142
|
-
var
|
|
157
|
+
var _this$props3 = this.props,
|
|
158
|
+
iframeProps = _this$props3.iframeProps,
|
|
159
|
+
waitForReadyToUse = _this$props3.waitForReadyToUse;
|
|
143
160
|
var _this$state = this.state,
|
|
144
161
|
error = _this$state.error,
|
|
145
|
-
|
|
162
|
+
frameLoaded = _this$state.frameLoaded,
|
|
163
|
+
readyToUse = _this$state.readyToUse;
|
|
164
|
+
var loading = error === null && (!frameLoaded || waitForReadyToUse && !readyToUse);
|
|
146
165
|
return /*#__PURE__*/_react.default.createElement("div", (0, _extends2.default)({
|
|
147
166
|
ref: function ref(intentViewer) {
|
|
148
167
|
return _this3.intentViewer = intentViewer;
|
|
149
168
|
},
|
|
150
169
|
className: styles.intentContainer,
|
|
151
|
-
"aria-busy": loading
|
|
152
|
-
|
|
170
|
+
"aria-busy": loading,
|
|
171
|
+
"data-iframe-loaded": frameLoaded,
|
|
172
|
+
"data-waiting-for-ready-to-use": waitForReadyToUse && !readyToUse
|
|
173
|
+
}, (0, _get.default)(iframeProps, 'wrapperProps')), loading && /*#__PURE__*/_react.default.createElement("div", {
|
|
174
|
+
className: styles.intentContainer__loader
|
|
175
|
+
}, /*#__PURE__*/_react.default.createElement(_Spinner.default, (0, _extends2.default)({
|
|
153
176
|
size: "xxlarge"
|
|
154
|
-
}, (0, _get.default)(iframeProps, 'spinnerProps'))), error && /*#__PURE__*/_react.default.createElement("div", {
|
|
177
|
+
}, (0, _get.default)(iframeProps, 'spinnerProps')))), error && /*#__PURE__*/_react.default.createElement("div", {
|
|
155
178
|
className: styles.intentContainer__error
|
|
156
179
|
}, error.message));
|
|
157
180
|
}
|
|
@@ -177,10 +200,12 @@ IntentIframe.propTypes = {
|
|
|
177
200
|
iframeProps: iframeProps,
|
|
178
201
|
onHideCross: _propTypes.default.func,
|
|
179
202
|
onShowCross: _propTypes.default.func,
|
|
180
|
-
onReadyToUse: _propTypes.default.func
|
|
203
|
+
onReadyToUse: _propTypes.default.func,
|
|
204
|
+
waitForReadyToUse: _propTypes.default.bool
|
|
181
205
|
};
|
|
182
206
|
IntentIframe.defaultProps = {
|
|
183
|
-
data: {}
|
|
207
|
+
data: {},
|
|
208
|
+
waitForReadyToUse: false
|
|
184
209
|
};
|
|
185
210
|
|
|
186
211
|
var _default = (0, _cozyClient.withClient)(IntentIframe);
|
|
@@ -10,6 +10,7 @@ declare namespace IntentDialogOpener {
|
|
|
10
10
|
export let onComplete: PropTypes.Requireable<(...args: any[]) => any>;
|
|
11
11
|
export let onDismiss: PropTypes.Requireable<(...args: any[]) => any>;
|
|
12
12
|
export let onReadyToUse: PropTypes.Requireable<(...args: any[]) => any>;
|
|
13
|
+
export let waitForReadyToUse: PropTypes.Requireable<boolean>;
|
|
13
14
|
export let action: PropTypes.Validator<string>;
|
|
14
15
|
export let doctype: PropTypes.Validator<string>;
|
|
15
16
|
export let closable: PropTypes.Validator<boolean>;
|
|
@@ -26,6 +27,8 @@ declare namespace IntentDialogOpener {
|
|
|
26
27
|
export { Dialog as Component };
|
|
27
28
|
let showCloseButton_1: boolean;
|
|
28
29
|
export { showCloseButton_1 as showCloseButton };
|
|
30
|
+
let waitForReadyToUse_1: boolean;
|
|
31
|
+
export { waitForReadyToUse_1 as waitForReadyToUse };
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
import PropTypes from 'prop-types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/stylesheet.css
CHANGED
|
@@ -409,11 +409,31 @@ iframe {
|
|
|
409
409
|
justify-content: center;
|
|
410
410
|
align-items: center;
|
|
411
411
|
}
|
|
412
|
+
.styles__intentContainer___5Zqqq {
|
|
413
|
+
position: relative;
|
|
414
|
+
}
|
|
415
|
+
.styles__intentContainer__loader___1krAq {
|
|
416
|
+
display: flex;
|
|
417
|
+
justify-content: center;
|
|
418
|
+
align-items: center;
|
|
419
|
+
}
|
|
420
|
+
.styles__intentContainer___5Zqqq[data-iframe-loaded=true] .styles__intentContainer__loader___1krAq {
|
|
421
|
+
position: absolute;
|
|
422
|
+
z-index: 1;
|
|
423
|
+
top: 0;
|
|
424
|
+
right: 0;
|
|
425
|
+
bottom: 0;
|
|
426
|
+
left: 0;
|
|
427
|
+
background-color: var(--paperBackgroundColor);
|
|
428
|
+
}
|
|
412
429
|
/* Don't mess with the spinner centering until the iframe is fully loaded. */
|
|
413
|
-
.styles__intentContainer___5Zqqq[
|
|
430
|
+
.styles__intentContainer___5Zqqq[data-iframe-loaded=false] iframe {
|
|
414
431
|
height: 0;
|
|
415
432
|
width: 0;
|
|
416
433
|
}
|
|
434
|
+
.styles__intentContainer___5Zqqq[data-waiting-for-ready-to-use=true] iframe {
|
|
435
|
+
visibility: hidden;
|
|
436
|
+
}
|
|
417
437
|
.styles__intentContainer__error___YO3Lz {
|
|
418
438
|
color: var(--errorColor);
|
|
419
439
|
font-size: 1.5em;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozy-ui-plus",
|
|
3
|
-
"version": "12.1
|
|
3
|
+
"version": "12.2.1",
|
|
4
4
|
"description": "Cozy-ui-plus is an extension of cozy-ui and provides components based on cozy-client",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"cozy-client": "^60.21.3",
|
|
37
37
|
"cozy-device-helper": "2.0.0",
|
|
38
38
|
"cozy-intent": "^2.31.1",
|
|
39
|
-
"cozy-interapp": "^0.18.
|
|
39
|
+
"cozy-interapp": "^0.18.2",
|
|
40
40
|
"cozy-logger": "^1.18.1",
|
|
41
|
-
"cozy-sharing": "^37.2.
|
|
41
|
+
"cozy-sharing": "^37.2.5",
|
|
42
42
|
"cozy-stack-client": "^60.21.1",
|
|
43
43
|
"cozy-ui": "^138.1.0",
|
|
44
44
|
"jest": "30.3.0",
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
"react-dom": "^16 || ^17 || ^18",
|
|
79
79
|
"twake-i18n": ">=0.3.0"
|
|
80
80
|
},
|
|
81
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "663ae9860187ebdc6d6a2cfa2e7919d8a39c824f"
|
|
82
82
|
}
|
|
@@ -16,6 +16,7 @@ import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
|
|
|
16
16
|
fullWidth // extra props will be passed to the dialog
|
|
17
17
|
maxWidth="md" // extra props will be passed to the dialog
|
|
18
18
|
iframeProps={{ spinnerProps: { middle: true } }}
|
|
19
|
+
waitForReadyToUse
|
|
19
20
|
onComplete={res => alert('intent has completed ! ' + JSON.stringify(res))}
|
|
20
21
|
onDismiss={() => alert('intent has been dismissed !')}
|
|
21
22
|
>
|
|
@@ -23,3 +24,7 @@ import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
|
|
|
23
24
|
</IntentDialogOpener>
|
|
24
25
|
</DemoProvider>
|
|
25
26
|
```
|
|
27
|
+
|
|
28
|
+
Set `waitForReadyToUse` to keep the intent hidden behind the loading spinner
|
|
29
|
+
until its service sends the `readyToUse` signal. This option is disabled by
|
|
30
|
+
default because older intent services do not send this signal.
|
|
@@ -23,6 +23,7 @@ const IntentDialogOpener = props => {
|
|
|
23
23
|
onComplete,
|
|
24
24
|
onDismiss,
|
|
25
25
|
onReadyToUse,
|
|
26
|
+
waitForReadyToUse,
|
|
26
27
|
iframeProps,
|
|
27
28
|
Component,
|
|
28
29
|
...componentProps
|
|
@@ -70,6 +71,7 @@ const IntentDialogOpener = props => {
|
|
|
70
71
|
onCancel={handleDismiss}
|
|
71
72
|
onTerminate={handleComplete}
|
|
72
73
|
onReadyToUse={onReadyToUse}
|
|
74
|
+
waitForReadyToUse={waitForReadyToUse}
|
|
73
75
|
iframeProps={iframeProps}
|
|
74
76
|
/>
|
|
75
77
|
</Component>
|
|
@@ -88,6 +90,8 @@ IntentDialogOpener.propTypes = {
|
|
|
88
90
|
onDismiss: PropTypes.func,
|
|
89
91
|
/** Called when the intent service signals it is ready to use */
|
|
90
92
|
onReadyToUse: PropTypes.func,
|
|
93
|
+
/** Keep the loading state until the intent service signals it is ready to use */
|
|
94
|
+
waitForReadyToUse: PropTypes.bool,
|
|
91
95
|
/** Action you want to execute */
|
|
92
96
|
action: PropTypes.string.isRequired,
|
|
93
97
|
/** Doctype on which you want to execute the action */
|
|
@@ -108,7 +112,8 @@ IntentDialogOpener.defaultProps = {
|
|
|
108
112
|
tag: 'span',
|
|
109
113
|
closable: true,
|
|
110
114
|
Component: Dialog,
|
|
111
|
-
showCloseButton: true
|
|
115
|
+
showCloseButton: true,
|
|
116
|
+
waitForReadyToUse: false
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
export default IntentDialogOpener
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import IntentDialogOpener from './index'
|
|
5
|
+
|
|
6
|
+
jest.mock('cozy-ui/transpiled/react/CozyDialogs', () => ({
|
|
7
|
+
DialogCloseButton: () => null
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
jest.mock('cozy-ui/transpiled/react/Dialog', () => ({ children }) => (
|
|
11
|
+
<div>{children}</div>
|
|
12
|
+
))
|
|
13
|
+
|
|
14
|
+
jest.mock('../IntentIframe', () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: ({ waitForReadyToUse }) => (
|
|
17
|
+
<div
|
|
18
|
+
data-testid="intent-iframe"
|
|
19
|
+
data-wait-for-ready-to-use={waitForReadyToUse}
|
|
20
|
+
/>
|
|
21
|
+
),
|
|
22
|
+
iframeProps: require('prop-types').shape({})
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
describe('IntentDialogOpener', () => {
|
|
26
|
+
it('forwards waitForReadyToUse to the intent iframe', () => {
|
|
27
|
+
const { getByRole, getByTestId } = render(
|
|
28
|
+
<IntentDialogOpener
|
|
29
|
+
action="PICK"
|
|
30
|
+
doctype="io.cozy.files"
|
|
31
|
+
waitForReadyToUse
|
|
32
|
+
>
|
|
33
|
+
<button type="button">Open</button>
|
|
34
|
+
</IntentDialogOpener>
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
fireEvent.click(getByRole('button', { name: 'Open' }))
|
|
38
|
+
|
|
39
|
+
expect(getByTestId('intent-iframe')).toHaveAttribute(
|
|
40
|
+
'data-wait-for-ready-to-use',
|
|
41
|
+
'true'
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -19,9 +19,14 @@ import utils from '../../docs/utils'
|
|
|
19
19
|
onCancel={() => alert('intent cancelled')}
|
|
20
20
|
onError={(error) => alert('intent has failed with error: ' + error.message)}
|
|
21
21
|
onTerminate={doc => alert('intent has completed ! ' + JSON.stringify(doc))}
|
|
22
|
+
waitForReadyToUse
|
|
22
23
|
/>
|
|
23
24
|
```
|
|
24
25
|
|
|
26
|
+
Set `waitForReadyToUse` to keep the intent hidden behind the loading spinner
|
|
27
|
+
until its service sends the `readyToUse` signal. This option is disabled by
|
|
28
|
+
default because older intent services do not send this signal.
|
|
29
|
+
|
|
25
30
|
### Within an application side controled Dialog
|
|
26
31
|
|
|
27
32
|
Sometimes you have to render an Intent inside a modal, and handle the modal opening state on the application side. You can then use IntentIframe wrapped in a Dialog for that use case, instead of using IntentDialogOpener component (that is useful to start a new intent modal from a click on a button).
|
|
@@ -18,7 +18,7 @@ const DEFAULT_DATA = {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
class IntentIframe extends React.Component {
|
|
21
|
-
state = {
|
|
21
|
+
state = { error: null, frameLoaded: false, readyToUse: false }
|
|
22
22
|
|
|
23
23
|
componentDidMount() {
|
|
24
24
|
const { action, data, type, onCancel, onError, onTerminate, client } =
|
|
@@ -36,7 +36,6 @@ class IntentIframe extends React.Component {
|
|
|
36
36
|
create = intents.create
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
this.setState({ loading: true })
|
|
40
39
|
create(action, type, {
|
|
41
40
|
...DEFAULT_DATA,
|
|
42
41
|
...data
|
|
@@ -45,7 +44,7 @@ class IntentIframe extends React.Component {
|
|
|
45
44
|
onReady: this.onFrameLoaded,
|
|
46
45
|
onHideCross: this.props.onHideCross,
|
|
47
46
|
onShowCross: this.props.onShowCross,
|
|
48
|
-
onReadyToUse: this.
|
|
47
|
+
onReadyToUse: this.onReadyToUse
|
|
49
48
|
})
|
|
50
49
|
.then(result => {
|
|
51
50
|
// eslint-disable-next-line promise/always-return
|
|
@@ -53,29 +52,51 @@ class IntentIframe extends React.Component {
|
|
|
53
52
|
})
|
|
54
53
|
.catch(error => {
|
|
55
54
|
onError?.(error)
|
|
56
|
-
this.setState({ error
|
|
57
|
-
this.
|
|
55
|
+
this.setState({ error })
|
|
56
|
+
this.setIsLoading(false)
|
|
58
57
|
})
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
onFrameLoaded = () => {
|
|
62
|
-
this.setState({
|
|
63
|
-
|
|
61
|
+
this.setState({ frameLoaded: true }, () => {
|
|
62
|
+
if (!this.props.waitForReadyToUse || this.state.readyToUse) {
|
|
63
|
+
this.setIsLoading(false)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onReadyToUse = () => {
|
|
69
|
+
this.setState({ readyToUse: true }, () => {
|
|
70
|
+
if (this.props.waitForReadyToUse && this.state.frameLoaded) {
|
|
71
|
+
this.setIsLoading(false)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
this.props.onReadyToUse?.()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setIsLoading = isLoading => {
|
|
78
|
+
this.props.iframeProps?.setIsLoading?.(isLoading)
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
render() {
|
|
67
|
-
const { iframeProps } = this.props
|
|
68
|
-
const { error,
|
|
82
|
+
const { iframeProps, waitForReadyToUse } = this.props
|
|
83
|
+
const { error, frameLoaded, readyToUse } = this.state
|
|
84
|
+
const loading =
|
|
85
|
+
error === null && (!frameLoaded || (waitForReadyToUse && !readyToUse))
|
|
69
86
|
|
|
70
87
|
return (
|
|
71
88
|
<div
|
|
72
89
|
ref={intentViewer => (this.intentViewer = intentViewer)}
|
|
73
90
|
className={styles.intentContainer}
|
|
74
91
|
aria-busy={loading}
|
|
92
|
+
data-iframe-loaded={frameLoaded}
|
|
93
|
+
data-waiting-for-ready-to-use={waitForReadyToUse && !readyToUse}
|
|
75
94
|
{...get(iframeProps, 'wrapperProps')}
|
|
76
95
|
>
|
|
77
96
|
{loading && (
|
|
78
|
-
<
|
|
97
|
+
<div className={styles.intentContainer__loader}>
|
|
98
|
+
<Spinner size="xxlarge" {...get(iframeProps, 'spinnerProps')} />
|
|
99
|
+
</div>
|
|
79
100
|
)}
|
|
80
101
|
{error && (
|
|
81
102
|
<div className={styles.intentContainer__error}>{error.message}</div>
|
|
@@ -103,11 +124,13 @@ IntentIframe.propTypes = {
|
|
|
103
124
|
iframeProps: iframeProps,
|
|
104
125
|
onHideCross: PropTypes.func,
|
|
105
126
|
onShowCross: PropTypes.func,
|
|
106
|
-
onReadyToUse: PropTypes.func
|
|
127
|
+
onReadyToUse: PropTypes.func,
|
|
128
|
+
waitForReadyToUse: PropTypes.bool
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
IntentIframe.defaultProps = {
|
|
110
|
-
data: {}
|
|
132
|
+
data: {},
|
|
133
|
+
waitForReadyToUse: false
|
|
111
134
|
}
|
|
112
135
|
|
|
113
136
|
export default withClient(IntentIframe)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { act, render, waitFor } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import IntentIframe from './index'
|
|
5
|
+
|
|
6
|
+
jest.mock('cozy-ui/transpiled/react/Spinner', () => () => (
|
|
7
|
+
<div data-testid="intent-spinner" />
|
|
8
|
+
))
|
|
9
|
+
|
|
10
|
+
function setup({ startError = null, waitForReadyToUse = false } = {}) {
|
|
11
|
+
let startOptions
|
|
12
|
+
const start = jest.fn((element, options) => {
|
|
13
|
+
startOptions = options
|
|
14
|
+
element.appendChild(document.createElement('iframe'))
|
|
15
|
+
return startError ? Promise.reject(startError) : new Promise(() => {})
|
|
16
|
+
})
|
|
17
|
+
const create = jest.fn(() => ({ start }))
|
|
18
|
+
const onError = jest.fn()
|
|
19
|
+
const onReadyToUse = jest.fn()
|
|
20
|
+
const setIsLoading = jest.fn()
|
|
21
|
+
|
|
22
|
+
const result = render(
|
|
23
|
+
<IntentIframe
|
|
24
|
+
action="PICK"
|
|
25
|
+
client={{}}
|
|
26
|
+
create={create}
|
|
27
|
+
iframeProps={{ setIsLoading }}
|
|
28
|
+
onCancel={jest.fn()}
|
|
29
|
+
onError={onError}
|
|
30
|
+
onReadyToUse={onReadyToUse}
|
|
31
|
+
onTerminate={jest.fn()}
|
|
32
|
+
type="io.cozy.files"
|
|
33
|
+
waitForReadyToUse={waitForReadyToUse}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
getStartOptions: () => startOptions,
|
|
40
|
+
onError,
|
|
41
|
+
onReadyToUse,
|
|
42
|
+
setIsLoading
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('IntentIframe', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
jest.restoreAllMocks()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('keeps the iframe rendered behind the spinner until it is ready to use', () => {
|
|
56
|
+
const {
|
|
57
|
+
container,
|
|
58
|
+
getStartOptions,
|
|
59
|
+
onReadyToUse,
|
|
60
|
+
queryByTestId,
|
|
61
|
+
setIsLoading
|
|
62
|
+
} = setup({ waitForReadyToUse: true })
|
|
63
|
+
|
|
64
|
+
act(() => getStartOptions().onReady())
|
|
65
|
+
|
|
66
|
+
expect(queryByTestId('intent-spinner')).toBeInTheDocument()
|
|
67
|
+
expect(container.querySelector('iframe')).toBeInTheDocument()
|
|
68
|
+
expect(
|
|
69
|
+
container.querySelector(
|
|
70
|
+
'[data-iframe-loaded="true"][data-waiting-for-ready-to-use="true"]'
|
|
71
|
+
)
|
|
72
|
+
).toBeInTheDocument()
|
|
73
|
+
expect(setIsLoading).not.toHaveBeenCalled()
|
|
74
|
+
|
|
75
|
+
act(() => getStartOptions().onReadyToUse())
|
|
76
|
+
|
|
77
|
+
expect(queryByTestId('intent-spinner')).toBe(null)
|
|
78
|
+
expect(onReadyToUse).toHaveBeenCalledTimes(1)
|
|
79
|
+
expect(setIsLoading).toHaveBeenCalledWith(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('waits for the iframe when readyToUse is received first', () => {
|
|
83
|
+
const { getStartOptions, queryByTestId, setIsLoading } = setup({
|
|
84
|
+
waitForReadyToUse: true
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
act(() => getStartOptions().onReadyToUse())
|
|
88
|
+
|
|
89
|
+
expect(queryByTestId('intent-spinner')).toBeInTheDocument()
|
|
90
|
+
expect(setIsLoading).not.toHaveBeenCalled()
|
|
91
|
+
|
|
92
|
+
act(() => getStartOptions().onReady())
|
|
93
|
+
|
|
94
|
+
expect(queryByTestId('intent-spinner')).toBe(null)
|
|
95
|
+
expect(setIsLoading).toHaveBeenCalledWith(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('hides the spinner when the iframe loads by default', () => {
|
|
99
|
+
const { getStartOptions, queryByTestId, setIsLoading } = setup()
|
|
100
|
+
|
|
101
|
+
act(() => getStartOptions().onReady())
|
|
102
|
+
act(() => getStartOptions().onReadyToUse())
|
|
103
|
+
|
|
104
|
+
expect(queryByTestId('intent-spinner')).toBe(null)
|
|
105
|
+
expect(setIsLoading).toHaveBeenCalledTimes(1)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('shows intent errors instead of waiting for readyToUse', async () => {
|
|
109
|
+
const error = new Error('Intent failed')
|
|
110
|
+
const { container, onError, queryByTestId, setIsLoading } = setup({
|
|
111
|
+
startError: error,
|
|
112
|
+
waitForReadyToUse: true
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(queryByTestId('intent-spinner')).toBe(null))
|
|
116
|
+
|
|
117
|
+
expect(container).toHaveTextContent('Intent failed')
|
|
118
|
+
expect(onError).toHaveBeenCalledWith(error)
|
|
119
|
+
expect(setIsLoading).toHaveBeenCalledWith(false)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -11,11 +11,31 @@ iframe
|
|
|
11
11
|
justify-content center
|
|
12
12
|
align-items center
|
|
13
13
|
|
|
14
|
+
.intentContainer
|
|
15
|
+
position relative
|
|
16
|
+
|
|
17
|
+
.intentContainer__loader
|
|
18
|
+
display flex
|
|
19
|
+
justify-content center
|
|
20
|
+
align-items center
|
|
21
|
+
|
|
22
|
+
.intentContainer[data-iframe-loaded=true] .intentContainer__loader
|
|
23
|
+
position absolute
|
|
24
|
+
z-index 1
|
|
25
|
+
top 0
|
|
26
|
+
right 0
|
|
27
|
+
bottom 0
|
|
28
|
+
left 0
|
|
29
|
+
background-color var(--paperBackgroundColor)
|
|
30
|
+
|
|
14
31
|
/* Don't mess with the spinner centering until the iframe is fully loaded. */
|
|
15
|
-
.intentContainer[
|
|
32
|
+
.intentContainer[data-iframe-loaded=false] iframe
|
|
16
33
|
height 0
|
|
17
34
|
width 0
|
|
18
35
|
|
|
36
|
+
.intentContainer[data-waiting-for-ready-to-use=true] iframe
|
|
37
|
+
visibility hidden
|
|
38
|
+
|
|
19
39
|
.intentContainer__error
|
|
20
40
|
color var(--errorColor)
|
|
21
41
|
font-size 1.5em
|