@atlaskit/smart-user-picker 5.0.1 → 5.0.4

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @atlassian/smart-user-picker
2
2
 
3
+ ## 5.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`f805f47c19a`](https://bitbucket.org/atlassian/atlassian-frontend/commits/f805f47c19a) - Smart User Picker now catches errors emitted from the optional `onError` fallback data source, and also now only sends a UFO failure event if the primary data source (URS) fails AND the `onError` prop either fails or is not provided.
8
+
9
+ ## 5.0.3
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+
15
+ ## 5.0.2
16
+
17
+ ### Patch Changes
18
+
19
+ - [`6fc78303271`](https://bitbucket.org/atlassian/atlassian-frontend/commits/6fc78303271) - UFO measurement of how long it takes the list of users to be shown
20
+
3
21
  ## 5.0.1
4
22
 
5
23
  ### Patch Changes
@@ -39,6 +39,8 @@ var _memoizeOne = _interopRequireDefault(require("memoize-one"));
39
39
 
40
40
  var _reactIntlNext = require("react-intl-next");
41
41
 
42
+ var _ufo = require("@atlaskit/ufo");
43
+
42
44
  var _userPicker = _interopRequireDefault(require("@atlaskit/user-picker"));
43
45
 
44
46
  var _analytics = require("../analytics");
@@ -47,6 +49,8 @@ var _MessagesIntlProvider = _interopRequireDefault(require("./MessagesIntlProvid
47
49
 
48
50
  var _service = require("../service");
49
51
 
52
+ var _ufoExperiences = require("../ufoExperiences");
53
+
50
54
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
51
55
 
52
56
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
@@ -89,33 +93,39 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
89
93
 
90
94
  var _super = _createSuper(SmartUserPickerWithoutAnalytics);
91
95
 
92
- function SmartUserPickerWithoutAnalytics() {
96
+ function SmartUserPickerWithoutAnalytics(props) {
93
97
  var _this$props$debounceT;
94
98
 
95
99
  var _this;
96
100
 
97
101
  (0, _classCallCheck2.default)(this, SmartUserPickerWithoutAnalytics);
98
-
99
- for (var _len = arguments.length, _args = new Array(_len), _key = 0; _key < _len; _key++) {
100
- _args[_key] = arguments[_key];
101
- }
102
-
103
- _this = _super.call.apply(_super, [this].concat(_args));
102
+ _this = _super.call(this, props);
104
103
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "state", {
105
104
  users: [],
106
105
  loading: false,
107
- error: false,
108
106
  closed: true,
109
107
  query: '',
110
108
  defaultValue: [],
111
109
  bootstrapOptions: []
112
110
  });
111
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "abortOptionsShownUfoExperience", function () {
112
+ if (_this.optionsShownUfoExperienceInstance.state.id === _ufo.UFOExperienceState.STARTED.id) {
113
+ // There may be an existing UFO timing running from previous key entry or focus,
114
+ // so abort it and restart it just in case.
115
+ _this.optionsShownUfoExperienceInstance.abort();
116
+ }
117
+ });
118
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "startOptionsShownUfoExperience", function () {
119
+ _this.abortOptionsShownUfoExperience();
120
+
121
+ _this.optionsShownUfoExperienceInstance.start();
122
+ });
113
123
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "fireEvent", function (eventCreator) {
114
124
  var createAnalyticsEvent = _this.props.createAnalyticsEvent;
115
125
 
116
126
  if (createAnalyticsEvent) {
117
- for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
118
- args[_key2 - 1] = arguments[_key2];
127
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
128
+ args[_key - 1] = arguments[_key];
119
129
  }
120
130
 
121
131
  (0, _analytics.createAndFireEventInElementsChannel)(eventCreator.apply(void 0, [_this.props, _this.state].concat(args)))(createAnalyticsEvent);
@@ -126,21 +136,21 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
126
136
  });
127
137
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "memoizedFilterOptions", (0, _memoizeOne.default)(_this.filterOptions));
128
138
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "getUsers", (0, _debounce.default)( /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
129
- var _this$state, query, sessionId, _this$props, containerId, childObjectId, objectId, principalId, productKey, siteId, orgId, baseUrl, includeUsers, includeGroups, includeTeams, maxOptions, searchQueryFilter, onEmpty, productAttributes, intl, maxNumberOfResults, startTime, recommendationsRequest, _yield$onEmpty, recommendedUsers, elapsedTimeMilli, displayedUsers, defaultUsers, _elapsedTimeMilli;
139
+ var _this$state, query, sessionId, closed, _this$props, baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeUsers, intl, maxOptions, objectId, onEmpty, onError, orgId, principalId, productAttributes, productKey, searchQueryFilter, siteId, maxNumberOfResults, startTime, recommendationsRequest, _yield$onEmpty, recommendedUsers, elapsedTimeMilli, displayedUsers, onErrorProducedError, defaultUsers, _elapsedTimeMilli;
130
140
 
131
141
  return _regenerator.default.wrap(function _callee$(_context) {
132
142
  while (1) {
133
143
  switch (_context.prev = _context.next) {
134
144
  case 0:
135
- _this$state = _this.state, query = _this$state.query, sessionId = _this$state.sessionId;
136
- _this$props = _this.props, containerId = _this$props.containerId, childObjectId = _this$props.childObjectId, objectId = _this$props.objectId, principalId = _this$props.principalId, productKey = _this$props.productKey, siteId = _this$props.siteId, orgId = _this$props.orgId, baseUrl = _this$props.baseUrl, includeUsers = _this$props.includeUsers, includeGroups = _this$props.includeGroups, includeTeams = _this$props.includeTeams, maxOptions = _this$props.maxOptions, searchQueryFilter = _this$props.searchQueryFilter, onEmpty = _this$props.onEmpty, productAttributes = _this$props.productAttributes, intl = _this$props.intl;
145
+ _this$state = _this.state, query = _this$state.query, sessionId = _this$state.sessionId, closed = _this$state.closed;
146
+ _this$props = _this.props, baseUrl = _this$props.baseUrl, childObjectId = _this$props.childObjectId, containerId = _this$props.containerId, fieldId = _this$props.fieldId, includeGroups = _this$props.includeGroups, includeTeams = _this$props.includeTeams, includeUsers = _this$props.includeUsers, intl = _this$props.intl, maxOptions = _this$props.maxOptions, objectId = _this$props.objectId, onEmpty = _this$props.onEmpty, onError = _this$props.onError, orgId = _this$props.orgId, principalId = _this$props.principalId, productAttributes = _this$props.productAttributes, productKey = _this$props.productKey, searchQueryFilter = _this$props.searchQueryFilter, siteId = _this$props.siteId;
137
147
  maxNumberOfResults = maxOptions || 100;
138
148
  startTime = window.performance.now();
139
149
  recommendationsRequest = {
140
150
  baseUrl: baseUrl,
141
151
  context: {
142
152
  containerId: containerId,
143
- contextType: _this.props.fieldId,
153
+ contextType: fieldId,
144
154
  objectId: objectId,
145
155
  principalId: principalId,
146
156
  productKey: productKey,
@@ -230,41 +240,87 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
230
240
  };
231
241
  });
232
242
 
233
- _context.next = 40;
243
+ _context.next = 58;
234
244
  break;
235
245
 
236
246
  case 31:
237
247
  _context.prev = 31;
238
248
  _context.t4 = _context["catch"](5);
239
249
 
250
+ if (!closed && !onError) {
251
+ // If the user lookup fails while the menu is open, and the consumer is not providing a
252
+ // fallback data source via the onError prop, then send UFO failure
253
+ _this.optionsShownUfoExperienceInstance.failure();
254
+ }
255
+
240
256
  _this.setState({
241
- users: [],
242
- error: true
257
+ users: []
243
258
  });
244
259
 
245
- _context.next = 36;
246
- return _this.props.onError ? _this.props.onError(_context.t4, recommendationsRequest) || Promise.resolve([]) : Promise.resolve([]);
260
+ onErrorProducedError = false;
261
+ defaultUsers = [];
262
+ _context.prev = 37;
247
263
 
248
- case 36:
249
- defaultUsers = _context.sent;
250
- _elapsedTimeMilli = window.performance.now() - startTime;
264
+ if (!onError) {
265
+ _context.next = 47;
266
+ break;
267
+ }
268
+
269
+ _context.next = 41;
270
+ return onError(_context.t4, recommendationsRequest);
271
+
272
+ case 41:
273
+ _context.t6 = _context.sent;
274
+
275
+ if (_context.t6) {
276
+ _context.next = 44;
277
+ break;
278
+ }
279
+
280
+ _context.t6 = [];
281
+
282
+ case 44:
283
+ _context.t5 = _context.t6;
284
+ _context.next = 48;
285
+ break;
286
+
287
+ case 47:
288
+ _context.t5 = [];
289
+
290
+ case 48:
291
+ defaultUsers = _context.t5;
292
+ _context.next = 54;
293
+ break;
294
+
295
+ case 51:
296
+ _context.prev = 51;
297
+ _context.t7 = _context["catch"](37);
298
+ onErrorProducedError = true;
299
+
300
+ case 54:
301
+ if (onErrorProducedError) {
302
+ // Log error from fallback data source `onError` to UFO
303
+ _this.optionsShownUfoExperienceInstance.failure();
304
+ }
251
305
 
252
306
  _this.setState({
253
307
  users: defaultUsers,
254
308
  loading: false
255
309
  });
256
310
 
311
+ _elapsedTimeMilli = window.performance.now() - startTime;
312
+
257
313
  _this.fireEvent(_analytics.failedRequestUsersEvent, {
258
314
  elapsedTimeMilli: _elapsedTimeMilli,
259
315
  productAttributes: productAttributes
260
316
  });
261
317
 
262
- case 40:
318
+ case 58:
263
319
  case "end":
264
320
  return _context.stop();
265
321
  }
266
322
  }
267
- }, _callee, null, [[5, 31]]);
323
+ }, _callee, null, [[5, 31], [37, 51]]);
268
324
  })), (_this$props$debounceT = _this.props.debounceTime) !== null && _this$props$debounceT !== void 0 ? _this$props$debounceT : 0));
269
325
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "onInputChange", function (newQuery, sessionId) {
270
326
  var query = newQuery || '';
@@ -275,6 +331,12 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
275
331
  }
276
332
 
277
333
  if (!closed) {
334
+ // If the input has been typed into and the dropdown has not been closed
335
+ // (i.e. input blurred) then start the UFO timer.
336
+ // If there's a previous UFO timer running for the same "options shown" experience,
337
+ // it will be aborted first.
338
+ _this.startOptionsShownUfoExperience();
339
+
278
340
  _this.setState({
279
341
  loading: true,
280
342
  query: query,
@@ -329,6 +391,8 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
329
391
  closed: false
330
392
  };
331
393
 
394
+ _this.startOptionsShownUfoExperience();
395
+
332
396
  if (_this.state.users.length === 0) {
333
397
  state.sessionId = sessionId;
334
398
  state.loading = true;
@@ -349,7 +413,9 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
349
413
  }
350
414
  });
351
415
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "onBlur", function (sessionId) {
352
- _this.getUsers.cancel(); // clear old users if query is populated so that on refocus,
416
+ _this.getUsers.cancel();
417
+
418
+ _this.abortOptionsShownUfoExperience(); // clear old users if query is populated so that on refocus,
353
419
  // the old list is not shown
354
420
 
355
421
 
@@ -365,6 +431,7 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
365
431
  _this.props.onBlur(sessionId);
366
432
  }
367
433
  });
434
+ _this.optionsShownUfoExperienceInstance = _ufoExperiences.smartUserPickerOptionsShownUfoExperience.getInstance(props.inputId || props.fieldId);
368
435
  return _this;
369
436
  }
370
437
 
@@ -439,6 +506,12 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
439
506
 
440
507
  if ((this.state.sessionId !== prevState.sessionId || this.state.query !== prevState.query) && (this.state.query !== '' || !this.props.bootstrapOptions)) {
441
508
  this.getUsers();
509
+ } else if (!this.state.closed && !this.state.loading) {
510
+ // If the component has rendered (including its dropdown list) and it
511
+ // is not loading anything further, send the success UFO event
512
+ if (![_ufo.UFOExperienceState.FAILED.id, _ufo.UFOExperienceState.SUCCEEDED.id].includes(this.optionsShownUfoExperienceInstance.state.id)) {
513
+ this.optionsShownUfoExperienceInstance.success();
514
+ }
442
515
  }
443
516
  }
444
517
  }, {
@@ -460,7 +533,6 @@ var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Component) {
460
533
 
461
534
  exports.SmartUserPickerWithoutAnalytics = SmartUserPickerWithoutAnalytics;
462
535
  (0, _defineProperty2.default)(SmartUserPickerWithoutAnalytics, "defaultProps", {
463
- onError: function onError() {},
464
536
  baseUrl: '',
465
537
  includeUsers: true,
466
538
  includeGroups: false,
@@ -7,7 +7,7 @@ var _typeof = require("@babel/runtime/helpers/typeof");
7
7
  Object.defineProperty(exports, "__esModule", {
8
8
  value: true
9
9
  });
10
- exports.useUFOConcurrentExperience = exports.smartUserPickerRenderedUfoExperience = exports.UfoErrorBoundary = void 0;
10
+ exports.useUFOConcurrentExperience = exports.smartUserPickerRenderedUfoExperience = exports.smartUserPickerOptionsShownUfoExperience = exports.UfoErrorBoundary = void 0;
11
11
 
12
12
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
13
13
 
@@ -40,6 +40,14 @@ var smartUserPickerRenderedUfoExperience = new _ufo.ConcurrentExperience('smart-
40
40
  performanceType: _ufo.ExperiencePerformanceTypes.PageSegmentLoad
41
41
  });
42
42
  exports.smartUserPickerRenderedUfoExperience = smartUserPickerRenderedUfoExperience;
43
+ var smartUserPickerOptionsShownUfoExperience = new _ufo.ConcurrentExperience('smart-user-picker-options-shown', {
44
+ platform: {
45
+ component: COMPONENT_NAME
46
+ },
47
+ type: _ufo.ExperienceTypes.Operation,
48
+ performanceType: _ufo.ExperiencePerformanceTypes.InlineResult
49
+ });
50
+ exports.smartUserPickerOptionsShownUfoExperience = smartUserPickerOptionsShownUfoExperience;
43
51
 
44
52
  var useUFOConcurrentExperience = function useUFOConcurrentExperience(experience, id) {
45
53
  var experienceForId = experience.getInstance(id); // Equivalent to componentWillMount - replace with @atlaskit/ufo's
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-user-picker",
3
- "version": "5.0.1",
3
+ "version": "5.0.4",
4
4
  "sideEffects": false
5
5
  }
@@ -6,10 +6,12 @@ import { v4 as uuidV4 } from 'uuid';
6
6
  import { withAnalyticsEvents } from '@atlaskit/analytics-next';
7
7
  import memoizeOne from 'memoize-one';
8
8
  import { injectIntl } from 'react-intl-next';
9
+ import { UFOExperienceState } from '@atlaskit/ufo';
9
10
  import UserPicker from '@atlaskit/user-picker';
10
11
  import { requestUsersEvent, filterUsersEvent, preparedUsersLoadedEvent, successfulRequestUsersEvent, failedRequestUsersEvent, mountedWithPrefetchEvent, createAndFireEventInElementsChannel } from '../analytics';
11
12
  import MessagesIntlProvider from './MessagesIntlProvider';
12
13
  import { getUserRecommendations, hydrateDefaultValues } from '../service';
14
+ import { smartUserPickerOptionsShownUfoExperience } from '../ufoExperiences';
13
15
  const DEFAULT_DEBOUNCE_TIME_MS = 150;
14
16
 
15
17
  const hasContextChanged = (oldContext, newContext) => oldContext.siteId !== newContext.siteId || oldContext.orgId !== newContext.orgId || oldContext.productKey !== newContext.productKey || oldContext.principalId !== newContext.principalId || oldContext.containerId !== newContext.containerId || oldContext.objectId !== newContext.objectId || oldContext.childObjectId !== newContext.childObjectId;
@@ -35,21 +37,33 @@ const getUsersForAnalytics = users => (users || []).map(({
35
37
  }));
36
38
 
37
39
  export class SmartUserPickerWithoutAnalytics extends React.Component {
38
- constructor(..._args) {
40
+ constructor(props) {
39
41
  var _this$props$debounceT;
40
42
 
41
- super(..._args);
43
+ super(props);
42
44
 
43
45
  _defineProperty(this, "state", {
44
46
  users: [],
45
47
  loading: false,
46
- error: false,
47
48
  closed: true,
48
49
  query: '',
49
50
  defaultValue: [],
50
51
  bootstrapOptions: []
51
52
  });
52
53
 
54
+ _defineProperty(this, "abortOptionsShownUfoExperience", () => {
55
+ if (this.optionsShownUfoExperienceInstance.state.id === UFOExperienceState.STARTED.id) {
56
+ // There may be an existing UFO timing running from previous key entry or focus,
57
+ // so abort it and restart it just in case.
58
+ this.optionsShownUfoExperienceInstance.abort();
59
+ }
60
+ });
61
+
62
+ _defineProperty(this, "startOptionsShownUfoExperience", () => {
63
+ this.abortOptionsShownUfoExperience();
64
+ this.optionsShownUfoExperienceInstance.start();
65
+ });
66
+
53
67
  _defineProperty(this, "fireEvent", (eventCreator, ...args) => {
54
68
  const {
55
69
  createAnalyticsEvent
@@ -67,25 +81,28 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
67
81
  _defineProperty(this, "getUsers", debounce(async () => {
68
82
  const {
69
83
  query,
70
- sessionId
84
+ sessionId,
85
+ closed
71
86
  } = this.state;
72
87
  const {
73
- containerId,
74
- childObjectId,
75
- objectId,
76
- principalId,
77
- productKey,
78
- siteId,
79
- orgId,
80
88
  baseUrl,
81
- includeUsers,
89
+ childObjectId,
90
+ containerId,
91
+ fieldId,
82
92
  includeGroups,
83
93
  includeTeams,
94
+ includeUsers,
95
+ intl,
84
96
  maxOptions,
85
- searchQueryFilter,
97
+ objectId,
86
98
  onEmpty,
99
+ onError,
100
+ orgId,
101
+ principalId,
87
102
  productAttributes,
88
- intl
103
+ productKey,
104
+ searchQueryFilter,
105
+ siteId
89
106
  } = this.props;
90
107
  const maxNumberOfResults = maxOptions || 100;
91
108
  const startTime = window.performance.now();
@@ -93,7 +110,7 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
93
110
  baseUrl,
94
111
  context: {
95
112
  containerId,
96
- contextType: this.props.fieldId,
113
+ contextType: fieldId,
97
114
  objectId,
98
115
  principalId,
99
116
  productKey,
@@ -135,16 +152,34 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
135
152
  };
136
153
  });
137
154
  } catch (e) {
155
+ if (!closed && !onError) {
156
+ // If the user lookup fails while the menu is open, and the consumer is not providing a
157
+ // fallback data source via the onError prop, then send UFO failure
158
+ this.optionsShownUfoExperienceInstance.failure();
159
+ }
160
+
138
161
  this.setState({
139
- users: [],
140
- error: true
162
+ users: []
141
163
  });
142
- const defaultUsers = await (this.props.onError ? this.props.onError(e, recommendationsRequest) || Promise.resolve([]) : Promise.resolve([]));
143
- const elapsedTimeMilli = window.performance.now() - startTime;
164
+ let onErrorProducedError = false;
165
+ let defaultUsers = [];
166
+
167
+ try {
168
+ defaultUsers = onError ? (await onError(e, recommendationsRequest)) || [] : [];
169
+ } catch (error) {
170
+ onErrorProducedError = true;
171
+ }
172
+
173
+ if (onErrorProducedError) {
174
+ // Log error from fallback data source `onError` to UFO
175
+ this.optionsShownUfoExperienceInstance.failure();
176
+ }
177
+
144
178
  this.setState({
145
179
  users: defaultUsers,
146
180
  loading: false
147
181
  });
182
+ const elapsedTimeMilli = window.performance.now() - startTime;
148
183
  this.fireEvent(failedRequestUsersEvent, {
149
184
  elapsedTimeMilli,
150
185
  productAttributes
@@ -163,6 +198,11 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
163
198
  }
164
199
 
165
200
  if (!closed) {
201
+ // If the input has been typed into and the dropdown has not been closed
202
+ // (i.e. input blurred) then start the UFO timer.
203
+ // If there's a previous UFO timer running for the same "options shown" experience,
204
+ // it will be aborted first.
205
+ this.startOptionsShownUfoExperience();
166
206
  this.setState({
167
207
  loading: true,
168
208
  query,
@@ -211,6 +251,7 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
211
251
  query: '',
212
252
  closed: false
213
253
  };
254
+ this.startOptionsShownUfoExperience();
214
255
 
215
256
  if (this.state.users.length === 0) {
216
257
  state.sessionId = sessionId;
@@ -233,7 +274,8 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
233
274
  });
234
275
 
235
276
  _defineProperty(this, "onBlur", sessionId => {
236
- this.getUsers.cancel(); // clear old users if query is populated so that on refocus,
277
+ this.getUsers.cancel();
278
+ this.abortOptionsShownUfoExperience(); // clear old users if query is populated so that on refocus,
237
279
  // the old list is not shown
238
280
 
239
281
  const users = this.state.query.length === 0 ? this.state.users : [];
@@ -247,6 +289,8 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
247
289
  this.props.onBlur(sessionId);
248
290
  }
249
291
  });
292
+
293
+ this.optionsShownUfoExperienceInstance = smartUserPickerOptionsShownUfoExperience.getInstance(props.inputId || props.fieldId);
250
294
  }
251
295
 
252
296
  async componentDidMount() {
@@ -286,6 +330,12 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
286
330
 
287
331
  if ((this.state.sessionId !== prevState.sessionId || this.state.query !== prevState.query) && (this.state.query !== '' || !this.props.bootstrapOptions)) {
288
332
  this.getUsers();
333
+ } else if (!this.state.closed && !this.state.loading) {
334
+ // If the component has rendered (including its dropdown list) and it
335
+ // is not loading anything further, send the success UFO event
336
+ if (![UFOExperienceState.FAILED.id, UFOExperienceState.SUCCEEDED.id].includes(this.optionsShownUfoExperienceInstance.state.id)) {
337
+ this.optionsShownUfoExperienceInstance.success();
338
+ }
289
339
  }
290
340
  }
291
341
 
@@ -303,7 +353,6 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
303
353
  } // TODO: Smart User picker team will have to add a type annotation here
304
354
 
305
355
  _defineProperty(SmartUserPickerWithoutAnalytics, "defaultProps", {
306
- onError: () => {},
307
356
  baseUrl: '',
308
357
  includeUsers: true,
309
358
  includeGroups: false,
@@ -9,6 +9,13 @@ export const smartUserPickerRenderedUfoExperience = new ConcurrentExperience('sm
9
9
  type: ExperienceTypes.Load,
10
10
  performanceType: ExperiencePerformanceTypes.PageSegmentLoad
11
11
  });
12
+ export const smartUserPickerOptionsShownUfoExperience = new ConcurrentExperience('smart-user-picker-options-shown', {
13
+ platform: {
14
+ component: COMPONENT_NAME
15
+ },
16
+ type: ExperienceTypes.Operation,
17
+ performanceType: ExperiencePerformanceTypes.InlineResult
18
+ });
12
19
  export const useUFOConcurrentExperience = (experience, id) => {
13
20
  const experienceForId = experience.getInstance(id); // Equivalent to componentWillMount - replace with @atlaskit/ufo's
14
21
  // useUFOComponentExperience when it supports ConcurrentExperience.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-user-picker",
3
- "version": "5.0.1",
3
+ "version": "5.0.4",
4
4
  "sideEffects": false
5
5
  }
@@ -23,10 +23,12 @@ import { v4 as uuidV4 } from 'uuid';
23
23
  import { withAnalyticsEvents } from '@atlaskit/analytics-next';
24
24
  import memoizeOne from 'memoize-one';
25
25
  import { injectIntl } from 'react-intl-next';
26
+ import { UFOExperienceState } from '@atlaskit/ufo';
26
27
  import UserPicker from '@atlaskit/user-picker';
27
28
  import { requestUsersEvent, filterUsersEvent, preparedUsersLoadedEvent, successfulRequestUsersEvent, failedRequestUsersEvent, mountedWithPrefetchEvent, createAndFireEventInElementsChannel } from '../analytics';
28
29
  import MessagesIntlProvider from './MessagesIntlProvider';
29
30
  import { getUserRecommendations, hydrateDefaultValues } from '../service';
31
+ import { smartUserPickerOptionsShownUfoExperience } from '../ufoExperiences';
30
32
  var DEFAULT_DEBOUNCE_TIME_MS = 150;
31
33
 
32
34
  var hasContextChanged = function hasContextChanged(oldContext, newContext) {
@@ -61,35 +63,44 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
61
63
 
62
64
  var _super = _createSuper(SmartUserPickerWithoutAnalytics);
63
65
 
64
- function SmartUserPickerWithoutAnalytics() {
66
+ function SmartUserPickerWithoutAnalytics(props) {
65
67
  var _this$props$debounceT;
66
68
 
67
69
  var _this;
68
70
 
69
71
  _classCallCheck(this, SmartUserPickerWithoutAnalytics);
70
72
 
71
- for (var _len = arguments.length, _args = new Array(_len), _key = 0; _key < _len; _key++) {
72
- _args[_key] = arguments[_key];
73
- }
74
-
75
- _this = _super.call.apply(_super, [this].concat(_args));
73
+ _this = _super.call(this, props);
76
74
 
77
75
  _defineProperty(_assertThisInitialized(_this), "state", {
78
76
  users: [],
79
77
  loading: false,
80
- error: false,
81
78
  closed: true,
82
79
  query: '',
83
80
  defaultValue: [],
84
81
  bootstrapOptions: []
85
82
  });
86
83
 
84
+ _defineProperty(_assertThisInitialized(_this), "abortOptionsShownUfoExperience", function () {
85
+ if (_this.optionsShownUfoExperienceInstance.state.id === UFOExperienceState.STARTED.id) {
86
+ // There may be an existing UFO timing running from previous key entry or focus,
87
+ // so abort it and restart it just in case.
88
+ _this.optionsShownUfoExperienceInstance.abort();
89
+ }
90
+ });
91
+
92
+ _defineProperty(_assertThisInitialized(_this), "startOptionsShownUfoExperience", function () {
93
+ _this.abortOptionsShownUfoExperience();
94
+
95
+ _this.optionsShownUfoExperienceInstance.start();
96
+ });
97
+
87
98
  _defineProperty(_assertThisInitialized(_this), "fireEvent", function (eventCreator) {
88
99
  var createAnalyticsEvent = _this.props.createAnalyticsEvent;
89
100
 
90
101
  if (createAnalyticsEvent) {
91
- for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
92
- args[_key2 - 1] = arguments[_key2];
102
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
103
+ args[_key - 1] = arguments[_key];
93
104
  }
94
105
 
95
106
  createAndFireEventInElementsChannel(eventCreator.apply(void 0, [_this.props, _this.state].concat(args)))(createAnalyticsEvent);
@@ -103,21 +114,21 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
103
114
  _defineProperty(_assertThisInitialized(_this), "memoizedFilterOptions", memoizeOne(_this.filterOptions));
104
115
 
105
116
  _defineProperty(_assertThisInitialized(_this), "getUsers", debounce( /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
106
- var _this$state, query, sessionId, _this$props, containerId, childObjectId, objectId, principalId, productKey, siteId, orgId, baseUrl, includeUsers, includeGroups, includeTeams, maxOptions, searchQueryFilter, onEmpty, productAttributes, intl, maxNumberOfResults, startTime, recommendationsRequest, _yield$onEmpty, recommendedUsers, elapsedTimeMilli, displayedUsers, defaultUsers, _elapsedTimeMilli;
117
+ var _this$state, query, sessionId, closed, _this$props, baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeUsers, intl, maxOptions, objectId, onEmpty, onError, orgId, principalId, productAttributes, productKey, searchQueryFilter, siteId, maxNumberOfResults, startTime, recommendationsRequest, _yield$onEmpty, recommendedUsers, elapsedTimeMilli, displayedUsers, onErrorProducedError, defaultUsers, _elapsedTimeMilli;
107
118
 
108
119
  return _regeneratorRuntime.wrap(function _callee$(_context) {
109
120
  while (1) {
110
121
  switch (_context.prev = _context.next) {
111
122
  case 0:
112
- _this$state = _this.state, query = _this$state.query, sessionId = _this$state.sessionId;
113
- _this$props = _this.props, containerId = _this$props.containerId, childObjectId = _this$props.childObjectId, objectId = _this$props.objectId, principalId = _this$props.principalId, productKey = _this$props.productKey, siteId = _this$props.siteId, orgId = _this$props.orgId, baseUrl = _this$props.baseUrl, includeUsers = _this$props.includeUsers, includeGroups = _this$props.includeGroups, includeTeams = _this$props.includeTeams, maxOptions = _this$props.maxOptions, searchQueryFilter = _this$props.searchQueryFilter, onEmpty = _this$props.onEmpty, productAttributes = _this$props.productAttributes, intl = _this$props.intl;
123
+ _this$state = _this.state, query = _this$state.query, sessionId = _this$state.sessionId, closed = _this$state.closed;
124
+ _this$props = _this.props, baseUrl = _this$props.baseUrl, childObjectId = _this$props.childObjectId, containerId = _this$props.containerId, fieldId = _this$props.fieldId, includeGroups = _this$props.includeGroups, includeTeams = _this$props.includeTeams, includeUsers = _this$props.includeUsers, intl = _this$props.intl, maxOptions = _this$props.maxOptions, objectId = _this$props.objectId, onEmpty = _this$props.onEmpty, onError = _this$props.onError, orgId = _this$props.orgId, principalId = _this$props.principalId, productAttributes = _this$props.productAttributes, productKey = _this$props.productKey, searchQueryFilter = _this$props.searchQueryFilter, siteId = _this$props.siteId;
114
125
  maxNumberOfResults = maxOptions || 100;
115
126
  startTime = window.performance.now();
116
127
  recommendationsRequest = {
117
128
  baseUrl: baseUrl,
118
129
  context: {
119
130
  containerId: containerId,
120
- contextType: _this.props.fieldId,
131
+ contextType: fieldId,
121
132
  objectId: objectId,
122
133
  principalId: principalId,
123
134
  productKey: productKey,
@@ -207,41 +218,87 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
207
218
  };
208
219
  });
209
220
 
210
- _context.next = 40;
221
+ _context.next = 58;
211
222
  break;
212
223
 
213
224
  case 31:
214
225
  _context.prev = 31;
215
226
  _context.t4 = _context["catch"](5);
216
227
 
228
+ if (!closed && !onError) {
229
+ // If the user lookup fails while the menu is open, and the consumer is not providing a
230
+ // fallback data source via the onError prop, then send UFO failure
231
+ _this.optionsShownUfoExperienceInstance.failure();
232
+ }
233
+
217
234
  _this.setState({
218
- users: [],
219
- error: true
235
+ users: []
220
236
  });
221
237
 
222
- _context.next = 36;
223
- return _this.props.onError ? _this.props.onError(_context.t4, recommendationsRequest) || Promise.resolve([]) : Promise.resolve([]);
238
+ onErrorProducedError = false;
239
+ defaultUsers = [];
240
+ _context.prev = 37;
224
241
 
225
- case 36:
226
- defaultUsers = _context.sent;
227
- _elapsedTimeMilli = window.performance.now() - startTime;
242
+ if (!onError) {
243
+ _context.next = 47;
244
+ break;
245
+ }
246
+
247
+ _context.next = 41;
248
+ return onError(_context.t4, recommendationsRequest);
249
+
250
+ case 41:
251
+ _context.t6 = _context.sent;
252
+
253
+ if (_context.t6) {
254
+ _context.next = 44;
255
+ break;
256
+ }
257
+
258
+ _context.t6 = [];
259
+
260
+ case 44:
261
+ _context.t5 = _context.t6;
262
+ _context.next = 48;
263
+ break;
264
+
265
+ case 47:
266
+ _context.t5 = [];
267
+
268
+ case 48:
269
+ defaultUsers = _context.t5;
270
+ _context.next = 54;
271
+ break;
272
+
273
+ case 51:
274
+ _context.prev = 51;
275
+ _context.t7 = _context["catch"](37);
276
+ onErrorProducedError = true;
277
+
278
+ case 54:
279
+ if (onErrorProducedError) {
280
+ // Log error from fallback data source `onError` to UFO
281
+ _this.optionsShownUfoExperienceInstance.failure();
282
+ }
228
283
 
229
284
  _this.setState({
230
285
  users: defaultUsers,
231
286
  loading: false
232
287
  });
233
288
 
289
+ _elapsedTimeMilli = window.performance.now() - startTime;
290
+
234
291
  _this.fireEvent(failedRequestUsersEvent, {
235
292
  elapsedTimeMilli: _elapsedTimeMilli,
236
293
  productAttributes: productAttributes
237
294
  });
238
295
 
239
- case 40:
296
+ case 58:
240
297
  case "end":
241
298
  return _context.stop();
242
299
  }
243
300
  }
244
- }, _callee, null, [[5, 31]]);
301
+ }, _callee, null, [[5, 31], [37, 51]]);
245
302
  })), (_this$props$debounceT = _this.props.debounceTime) !== null && _this$props$debounceT !== void 0 ? _this$props$debounceT : 0));
246
303
 
247
304
  _defineProperty(_assertThisInitialized(_this), "onInputChange", function (newQuery, sessionId) {
@@ -253,6 +310,12 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
253
310
  }
254
311
 
255
312
  if (!closed) {
313
+ // If the input has been typed into and the dropdown has not been closed
314
+ // (i.e. input blurred) then start the UFO timer.
315
+ // If there's a previous UFO timer running for the same "options shown" experience,
316
+ // it will be aborted first.
317
+ _this.startOptionsShownUfoExperience();
318
+
256
319
  _this.setState({
257
320
  loading: true,
258
321
  query: query,
@@ -309,6 +372,8 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
309
372
  closed: false
310
373
  };
311
374
 
375
+ _this.startOptionsShownUfoExperience();
376
+
312
377
  if (_this.state.users.length === 0) {
313
378
  state.sessionId = sessionId;
314
379
  state.loading = true;
@@ -330,7 +395,9 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
330
395
  });
331
396
 
332
397
  _defineProperty(_assertThisInitialized(_this), "onBlur", function (sessionId) {
333
- _this.getUsers.cancel(); // clear old users if query is populated so that on refocus,
398
+ _this.getUsers.cancel();
399
+
400
+ _this.abortOptionsShownUfoExperience(); // clear old users if query is populated so that on refocus,
334
401
  // the old list is not shown
335
402
 
336
403
 
@@ -347,6 +414,7 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
347
414
  }
348
415
  });
349
416
 
417
+ _this.optionsShownUfoExperienceInstance = smartUserPickerOptionsShownUfoExperience.getInstance(props.inputId || props.fieldId);
350
418
  return _this;
351
419
  }
352
420
 
@@ -421,6 +489,12 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
421
489
 
422
490
  if ((this.state.sessionId !== prevState.sessionId || this.state.query !== prevState.query) && (this.state.query !== '' || !this.props.bootstrapOptions)) {
423
491
  this.getUsers();
492
+ } else if (!this.state.closed && !this.state.loading) {
493
+ // If the component has rendered (including its dropdown list) and it
494
+ // is not loading anything further, send the success UFO event
495
+ if (![UFOExperienceState.FAILED.id, UFOExperienceState.SUCCEEDED.id].includes(this.optionsShownUfoExperienceInstance.state.id)) {
496
+ this.optionsShownUfoExperienceInstance.success();
497
+ }
424
498
  }
425
499
  }
426
500
  }, {
@@ -441,7 +515,6 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
441
515
  }(React.Component); // TODO: Smart User picker team will have to add a type annotation here
442
516
 
443
517
  _defineProperty(SmartUserPickerWithoutAnalytics, "defaultProps", {
444
- onError: function onError() {},
445
518
  baseUrl: '',
446
519
  includeUsers: true,
447
520
  includeGroups: false,
@@ -19,6 +19,13 @@ export var smartUserPickerRenderedUfoExperience = new ConcurrentExperience('smar
19
19
  type: ExperienceTypes.Load,
20
20
  performanceType: ExperiencePerformanceTypes.PageSegmentLoad
21
21
  });
22
+ export var smartUserPickerOptionsShownUfoExperience = new ConcurrentExperience('smart-user-picker-options-shown', {
23
+ platform: {
24
+ component: COMPONENT_NAME
25
+ },
26
+ type: ExperienceTypes.Operation,
27
+ performanceType: ExperiencePerformanceTypes.InlineResult
28
+ });
22
29
  export var useUFOConcurrentExperience = function useUFOConcurrentExperience(experience, id) {
23
30
  var experienceForId = experience.getInstance(id); // Equivalent to componentWillMount - replace with @atlaskit/ufo's
24
31
  // useUFOComponentExperience when it supports ConcurrentExperience.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-user-picker",
3
- "version": "5.0.1",
3
+ "version": "5.0.4",
4
4
  "sideEffects": false
5
5
  }
@@ -1,12 +1,13 @@
1
1
  /// <reference types="lodash" />
2
2
  import React from 'react';
3
3
  import { WrappedComponentProps } from 'react-intl-next';
4
+ import { UFOExperience } from '@atlaskit/ufo';
4
5
  import { OptionData } from '@atlaskit/user-picker';
5
6
  import { Props, State, FilterOptions } from '../types';
6
7
  export declare class SmartUserPickerWithoutAnalytics extends React.Component<Props & WrappedComponentProps, State> {
7
8
  state: State;
9
+ optionsShownUfoExperienceInstance: UFOExperience;
8
10
  static defaultProps: {
9
- onError: () => void;
10
11
  baseUrl: string;
11
12
  includeUsers: boolean;
12
13
  includeGroups: boolean;
@@ -15,8 +16,11 @@ export declare class SmartUserPickerWithoutAnalytics extends React.Component<Pro
15
16
  principalId: string;
16
17
  debounceTime: number;
17
18
  };
19
+ constructor(props: Props & WrappedComponentProps);
18
20
  componentDidMount(): Promise<void>;
19
21
  componentDidUpdate(prevProps: Props, prevState: State): void;
22
+ abortOptionsShownUfoExperience: () => void;
23
+ startOptionsShownUfoExperience: () => void;
20
24
  private fireEvent;
21
25
  filterOptions: (users: OptionData[], query: string, propFilterOptions?: FilterOptions | undefined) => OptionData[];
22
26
  memoizedFilterOptions: import("memoize-one").MemoizedFn<(users: OptionData[], query: string, propFilterOptions?: FilterOptions | undefined) => OptionData[]>;
@@ -29,7 +29,6 @@ export interface State {
29
29
  users: OptionData[];
30
30
  loading: boolean;
31
31
  closed: boolean;
32
- error: boolean;
33
32
  query: string;
34
33
  sessionId?: string;
35
34
  defaultValue?: DefaultValue;
@@ -107,7 +106,7 @@ export interface SmartProps {
107
106
  */
108
107
  filterOptions?: FilterOptions;
109
108
  /**
110
- * Whether to include groups in the resultset. @default false
109
+ * Whether to include groups in the resultset. Only supported for Confluence. @default false
111
110
  */
112
111
  includeGroups?: boolean;
113
112
  /**
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { ConcurrentExperience } from '@atlaskit/ufo';
3
3
  export declare const smartUserPickerRenderedUfoExperience: ConcurrentExperience;
4
+ export declare const smartUserPickerOptionsShownUfoExperience: ConcurrentExperience;
4
5
  export declare const useUFOConcurrentExperience: (experience: ConcurrentExperience, id: string) => void;
5
6
  export declare class UfoErrorBoundary extends React.Component<{
6
7
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-user-picker",
3
- "version": "5.0.1",
3
+ "version": "5.0.4",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@atlaskit/analytics-next": "^8.2.0",
26
26
  "@atlaskit/ufo": "^0.1.0",
27
- "@atlaskit/user-picker": "^8.6.3",
27
+ "@atlaskit/user-picker": "^9.0.0",
28
28
  "@babel/runtime": "^7.0.0",
29
29
  "lodash": "^4.17.21",
30
30
  "memoize-one": "^6.0.0",
@@ -36,7 +36,7 @@
36
36
  "react-dom": "^16.8.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@atlaskit/button": "^16.0.0",
39
+ "@atlaskit/button": "^16.2.0",
40
40
  "@atlaskit/docs": "*",
41
41
  "@atlaskit/elements-test-helpers": "^0.7.0",
42
42
  "@atlaskit/modal-dialog": "^12.2.0",
@@ -50,7 +50,7 @@
50
50
  "fetch-mock": "^8.0.0",
51
51
  "graphql-tag": "^2.10.1",
52
52
  "mock-apollo-client": "^0.1.0",
53
- "typescript": "3.9.6"
53
+ "typescript": "3.9.10"
54
54
  },
55
55
  "prettier": "@atlassian/atlassian-frontend-prettier-config-1.0.1"
56
56
  }