@atlaskit/smart-user-picker 11.3.0 → 11.3.2

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,26 @@
1
1
  # @atlassian/smart-user-picker
2
2
 
3
+ ## 11.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`4bdae29c77827`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/4bdae29c77827) -
8
+ Fix suggestEmailsForDomain being suppressed for partial (non-email) input when enableEmailSearch
9
+ and allowEmailSelectionWhenEmailMatched=false are both set. The WAR-5498 FG cleanup (2026-03-04)
10
+ made the isEmailQuery restriction unconditional, breaking the domain-suggestion creatable option
11
+ (e.g. "asdf@atlassian.com"). Behind the new gate smart_user_picker_suggest_emails_for_domain,
12
+ partial queries now allow the domain suggestion to render, while the email option remains
13
+ suppressed when a full email query matches an existing user, or when the synthesized
14
+ "<query>@<domain>" address already belongs to a returned user.
15
+
16
+ ## 11.3.1
17
+
18
+ ### Patch Changes
19
+
20
+ - [`f834c7d669731`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/f834c7d669731) -
21
+ Removed feature gate TWCG-444 for Unified Share Dialogue experiment. No change in functionality
22
+ - Updated dependencies
23
+
3
24
  ## 11.3.0
4
25
 
5
26
  ### Minor Changes
@@ -12,7 +12,7 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
12
12
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } // eslint-disable-next-line @typescript-eslint/consistent-type-imports
13
13
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
14
14
  var packageName = "@atlaskit/smart-user-picker";
15
- var packageVersion = "11.2.0";
15
+ var packageVersion = "11.3.1";
16
16
  var startSession = exports.startSession = function startSession() {
17
17
  return {
18
18
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
@@ -32,7 +32,7 @@ var _MessagesIntlProvider = _interopRequireDefault(require("./MessagesIntlProvid
32
32
  var _types = require("../types");
33
33
  var _service = require("../service");
34
34
  var _ufoExperiences = require("../ufoExperiences");
35
- var _excluded = ["allowEmail", "enableEmailSearch", "allowEmailSelectionWhenEmailMatched"];
35
+ var _excluded = ["allowEmail", "enableEmailSearch", "allowEmailSelectionWhenEmailMatched", "suggestEmailsForDomain"];
36
36
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t1 in e) "default" !== _t1 && {}.hasOwnProperty.call(e, _t1) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t1)) && (i.get || i.set) ? o(f, _t1, i) : f[_t1] = e[_t1]); return f; })(e, t); }
37
37
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
38
38
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
@@ -93,6 +93,9 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
93
93
  });
94
94
  // Track if the last search was an email search that found matches
95
95
  (0, _defineProperty2.default)(_this, "lastEmailSearchFoundMatches", false);
96
+ // Track if the domain-suggested email (`<query>@<suggestEmailsForDomain>`) for a partial
97
+ // query already belongs to a returned user, in which case the suggestion is redundant
98
+ (0, _defineProperty2.default)(_this, "lastDomainSuggestionMatchedUser", false);
96
99
  (0, _defineProperty2.default)(_this, "abortOptionsShownUfoExperience", function () {
97
100
  if (_this.optionsShownUfoExperienceInstance.state.id === _ufo.UFOExperienceState.STARTED.id) {
98
101
  // There may be an existing UFO timing running from previous key entry or focus,
@@ -118,7 +121,7 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
118
121
  });
119
122
  (0, _defineProperty2.default)(_this, "memoizedFilterOptions", (0, _memoizeOne.default)(_this.filterOptions));
120
123
  (0, _defineProperty2.default)(_this, "getUsers", (0, _debounce.default)( /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
121
- var _this$state, query, sessionId, closed, _this$props, baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeTeamsUpdates, includeUsers, includeNonLicensedUsers, intl, fetchOptions, maxOptions, objectId, onEmpty, onError, overrideByline, displayEmailInByline, verifiedTeams, orgId, userbaseId, principalId, productAttributes, productKey, restrictTo, searchQueryFilter, siteId, isTeamSyncedToGroupDirectoryFilter, transformOptions, userResolvers, enableEmailSearch, maxNumberOfResults, startTime, isEmail, recommendationsRequest, _yield$onEmpty, _query, recommendedUsers, userRecommendationsPromise, userResolversPromises, _yield$Promise$all, _yield$Promise$all2, mainRecommendations, userResolverResults, _iterator, _step, option, _iterator2, _step2, _option, _iterator3, _step3, _option2, userMatches, elapsedTimeMilli, transformedOptions, displayedUsers, is5xxEvent, onErrorProducedError, defaultUsers, _elapsedTimeMilli, _t, _t2, _t3, _t4, _t5, _t6, _t7, _t8, _t9;
124
+ var _this$state, query, sessionId, closed, _this$props, baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeTeamsUpdates, includeUsers, includeNonLicensedUsers, intl, fetchOptions, maxOptions, objectId, onEmpty, onError, overrideByline, displayEmailInByline, verifiedTeams, orgId, userbaseId, principalId, productAttributes, productKey, restrictTo, searchQueryFilter, siteId, isTeamSyncedToGroupDirectoryFilter, transformOptions, userResolvers, enableEmailSearch, maxNumberOfResults, startTime, isEmail, recommendationsRequest, _yield$onEmpty, _query, recommendedUsers, userRecommendationsPromise, userResolversPromises, _yield$Promise$all, _yield$Promise$all2, mainRecommendations, userResolverResults, _iterator, _step, option, _iterator2, _step2, _option, _iterator3, _step3, _option2, userMatches, suggestEmailsForDomain, suggestedEmail, elapsedTimeMilli, transformedOptions, displayedUsers, is5xxEvent, onErrorProducedError, defaultUsers, _elapsedTimeMilli, _t, _t2, _t3, _t4, _t5, _t6, _t7, _t8, _t9;
122
125
  return _regenerator.default.wrap(function (_context) {
123
126
  while (1) switch (_context.prev = _context.next) {
124
127
  case 0:
@@ -176,13 +179,9 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
176
179
  return fetchOptions(_query);
177
180
  case 2:
178
181
  recommendedUsers = _context.sent;
179
- _context.next = 7;
182
+ _context.next = 5;
180
183
  break;
181
184
  case 3:
182
- if (!(0, _platformFeatureFlags.fg)('twcg-444-invite-usd-improvements-m2-gate')) {
183
- _context.next = 5;
184
- break;
185
- }
186
185
  userRecommendationsPromise = (0, _service.getUserRecommendations)(recommendationsRequest, intl);
187
186
  userResolversPromises = (userResolvers !== null && userResolvers !== void 0 ? userResolvers : []).map(function (resolver) {
188
187
  return resolver(_query).catch(function (error) {
@@ -201,14 +200,7 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
201
200
  mainRecommendations = _yield$Promise$all2[0];
202
201
  userResolverResults = _arrayLikeToArray(_yield$Promise$all2).slice(1);
203
202
  recommendedUsers = [mainRecommendations].concat((0, _toConsumableArray2.default)(userResolverResults)).flat();
204
- _context.next = 7;
205
- break;
206
203
  case 5:
207
- _context.next = 6;
208
- return (0, _service.getUserRecommendations)(recommendationsRequest, intl);
209
- case 6:
210
- recommendedUsers = _context.sent;
211
- case 7:
212
204
  if (overrideByline) {
213
205
  _iterator = _createForOfIteratorHelper(recommendedUsers);
214
206
  try {
@@ -270,52 +262,66 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
270
262
  } else {
271
263
  _this.lastEmailSearchFoundMatches = false;
272
264
  }
265
+
266
+ // For a partial query with a suggested domain, check whether the email we would
267
+ // synthesize (`<query>@<domain>`) already belongs to one of the returned users. If so,
268
+ // the real user option is shown instead and the email suggestion is suppressed.
269
+ suggestEmailsForDomain = _this.props.suggestEmailsForDomain;
270
+ if (suggestEmailsForDomain && !isEmail && _query) {
271
+ suggestedEmail = "".concat(_query, "@").concat(suggestEmailsForDomain).toLowerCase();
272
+ _this.lastDomainSuggestionMatchedUser = recommendedUsers.some(function (option) {
273
+ var _option$email;
274
+ return ((0, _userPicker.isUser)(option) || (0, _userPicker.isExternalUser)(option)) && ((_option$email = option.email) === null || _option$email === void 0 ? void 0 : _option$email.toLowerCase()) === suggestedEmail;
275
+ });
276
+ } else {
277
+ _this.lastDomainSuggestionMatchedUser = false;
278
+ }
273
279
  elapsedTimeMilli = window.performance.now() - startTime;
274
280
  if (!transformOptions) {
275
- _context.next = 9;
281
+ _context.next = 7;
276
282
  break;
277
283
  }
278
- _context.next = 8;
284
+ _context.next = 6;
279
285
  return transformOptions(recommendedUsers, _query);
280
- case 8:
286
+ case 6:
281
287
  _t = _context.sent;
282
- _context.next = 10;
288
+ _context.next = 8;
283
289
  break;
284
- case 9:
290
+ case 7:
285
291
  _t = recommendedUsers;
286
- case 10:
292
+ case 8:
287
293
  transformedOptions = _t;
288
294
  if (!(transformedOptions.length === 0 && onEmpty)) {
289
- _context.next = 15;
295
+ _context.next = 13;
290
296
  break;
291
297
  }
292
- _context.next = 11;
298
+ _context.next = 9;
293
299
  return onEmpty(_query);
294
- case 11:
300
+ case 9:
295
301
  _t4 = _yield$onEmpty = _context.sent;
296
302
  _t3 = _t4 !== null;
297
303
  if (!_t3) {
298
- _context.next = 12;
304
+ _context.next = 10;
299
305
  break;
300
306
  }
301
307
  _t3 = _yield$onEmpty !== void 0;
302
- case 12:
308
+ case 10:
303
309
  if (!_t3) {
304
- _context.next = 13;
310
+ _context.next = 11;
305
311
  break;
306
312
  }
307
313
  _t5 = _yield$onEmpty;
308
- _context.next = 14;
314
+ _context.next = 12;
309
315
  break;
310
- case 13:
316
+ case 11:
311
317
  _t5 = [];
312
- case 14:
318
+ case 12:
313
319
  _t2 = _t5;
314
- _context.next = 16;
320
+ _context.next = 14;
315
321
  break;
316
- case 15:
322
+ case 13:
317
323
  _t2 = transformedOptions;
318
- case 16:
324
+ case 14:
319
325
  displayedUsers = _t2;
320
326
  _this.setState(function (state) {
321
327
  var applicable = state.query === _query;
@@ -327,7 +333,7 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
327
333
  displayedUsers: getUsersForAnalytics(displayedUsers),
328
334
  productAttributes: productAttributes,
329
335
  applicable: applicable
330
- }, (0, _platformFeatureFlags.fg)('twcg-444-invite-usd-improvements-m2-gate') && {
336
+ }, {
331
337
  userResolvers: Array.isArray(userResolvers) ? userResolvers.map(function (resolver) {
332
338
  return resolver.name;
333
339
  }) : []
@@ -337,10 +343,10 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
337
343
  loading: loading
338
344
  };
339
345
  });
340
- _context.next = 25;
346
+ _context.next = 23;
341
347
  break;
342
- case 17:
343
- _context.prev = 17;
348
+ case 15:
349
+ _context.prev = 15;
344
350
  _t6 = _context["catch"](1);
345
351
  is5xxEvent = checkIf500Event(_t6.statusCode);
346
352
  if (!closed && !onError && is5xxEvent) {
@@ -353,35 +359,35 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
353
359
  });
354
360
  onErrorProducedError = false;
355
361
  defaultUsers = [];
356
- _context.prev = 18;
362
+ _context.prev = 16;
357
363
  if (!onError) {
358
- _context.next = 21;
364
+ _context.next = 19;
359
365
  break;
360
366
  }
361
- _context.next = 19;
367
+ _context.next = 17;
362
368
  return onError(_t6, recommendationsRequest);
363
- case 19:
369
+ case 17:
364
370
  _t8 = _context.sent;
365
371
  if (_t8) {
366
- _context.next = 20;
372
+ _context.next = 18;
367
373
  break;
368
374
  }
369
375
  _t8 = [];
370
- case 20:
376
+ case 18:
371
377
  _t7 = _t8;
372
- _context.next = 22;
378
+ _context.next = 20;
373
379
  break;
374
- case 21:
380
+ case 19:
375
381
  _t7 = [];
376
- case 22:
382
+ case 20:
377
383
  defaultUsers = _t7;
378
- _context.next = 24;
384
+ _context.next = 22;
379
385
  break;
380
- case 23:
381
- _context.prev = 23;
382
- _t9 = _context["catch"](18);
386
+ case 21:
387
+ _context.prev = 21;
388
+ _t9 = _context["catch"](16);
383
389
  onErrorProducedError = true;
384
- case 24:
390
+ case 22:
385
391
  if (onErrorProducedError && is5xxEvent) {
386
392
  // Log error from fallback data source `onError` to UFO
387
393
  _this.optionsShownUfoExperienceInstance.failure(ufoEndStateConfig(_this.props.fieldId));
@@ -398,11 +404,11 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
398
404
  elapsedTimeMilli: _elapsedTimeMilli,
399
405
  productAttributes: productAttributes
400
406
  });
401
- case 25:
407
+ case 23:
402
408
  case "end":
403
409
  return _context.stop();
404
410
  }
405
- }, _callee, null, [[1, 17], [18, 23]]);
411
+ }, _callee, null, [[1, 15], [16, 21]]);
406
412
  })), (_this$props$debounceT = _this.props.debounceTime) !== null && _this$props$debounceT !== void 0 ? _this$props$debounceT : 0));
407
413
  (0, _defineProperty2.default)(_this, "onInputChange", function (newQuery, sessionId) {
408
414
  var query = newQuery || '';
@@ -577,19 +583,34 @@ var SmartUserPickerWithoutAnalytics = exports.SmartUserPickerWithoutAnalytics =
577
583
  allowEmail = _this$props2.allowEmail,
578
584
  enableEmailSearch = _this$props2.enableEmailSearch,
579
585
  allowEmailSelectionWhenEmailMatched = _this$props2.allowEmailSelectionWhenEmailMatched,
586
+ suggestEmailsForDomain = _this$props2.suggestEmailsForDomain,
580
587
  restProps = (0, _objectWithoutProperties2.default)(_this$props2, _excluded);
581
588
 
582
589
  // Determine whether to allow email selection based on allowEmailSelectionWhenEmailMatched, if needed
583
590
  var shouldAllowEmail = allowEmail;
591
+
592
+ // When suggestEmailsForDomain is active, partial (non-email) input must still reach the base
593
+ // picker's domain-suggestion creatable logic, so the isEmailQuery restriction is relaxed for
594
+ // those queries only. Full email queries that matched an existing user remain suppressed.
595
+ var isDomainSuggestionEnabled = (0, _platformFeatureFlags.fg)('smart_user_picker_suggest_emails_for_domain') && !!suggestEmailsForDomain;
584
596
  if (allowEmail && enableEmailSearch && !allowEmailSelectionWhenEmailMatched) {
585
597
  var isCurrentQueryEmail = isEmailQuery(this.state.query);
586
- // Only allow email selection when:
587
- // 1. The query matches email format (validated by regex)
588
- // 2. No user/external user matches were found (only teams/groups suggested)
589
- shouldAllowEmail = isCurrentQueryEmail && !this.lastEmailSearchFoundMatches;
598
+ if (isDomainSuggestionEnabled) {
599
+ // Allow email for partial queries so the "<query>@<domain>" suggestion can render,
600
+ // but still suppress selection when a full email query matched an existing user,
601
+ // or when the synthesized "<query>@<domain>" is itself an existing user's email.
602
+ shouldAllowEmail = isCurrentQueryEmail ? !this.lastEmailSearchFoundMatches // full email: suppress when user matched
603
+ : !this.lastDomainSuggestionMatchedUser; // partial: suppress when synth email matched a user
604
+ } else {
605
+ // Only allow email selection when:
606
+ // 1. The query matches email format (validated by regex)
607
+ // 2. No user/external user matches were found (only teams/groups suggested)
608
+ shouldAllowEmail = isCurrentQueryEmail && !this.lastEmailSearchFoundMatches;
609
+ }
590
610
  }
591
611
  return /*#__PURE__*/_react.default.createElement(_MessagesIntlProvider.default, null, /*#__PURE__*/_react.default.createElement(_userPicker.default, (0, _extends2.default)({}, restProps, {
592
612
  allowEmail: shouldAllowEmail,
613
+ suggestEmailsForDomain: suggestEmailsForDomain,
593
614
  onInputChange: this.onInputChange,
594
615
  onBlur: this.onBlur,
595
616
  onFocus: this.onFocus,
@@ -3,7 +3,7 @@ import { createAndFireEvent } from '@atlaskit/analytics-next';
3
3
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
4
4
  import { v4 as uuid } from 'uuid';
5
5
  const packageName = "@atlaskit/smart-user-picker";
6
- const packageVersion = "11.2.0";
6
+ const packageVersion = "11.3.1";
7
7
  export const startSession = () => ({
8
8
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
9
9
  id: uuid(),
@@ -58,6 +58,9 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
58
58
  });
59
59
  // Track if the last search was an email search that found matches
60
60
  _defineProperty(this, "lastEmailSearchFoundMatches", false);
61
+ // Track if the domain-suggested email (`<query>@<suggestEmailsForDomain>`) for a partial
62
+ // query already belongs to a returned user, in which case the suggestion is redundant
63
+ _defineProperty(this, "lastDomainSuggestionMatchedUser", false);
61
64
  _defineProperty(this, "abortOptionsShownUfoExperience", () => {
62
65
  if (this.optionsShownUfoExperienceInstance.state.id === UFOExperienceState.STARTED.id) {
63
66
  // There may be an existing UFO timing running from previous key entry or focus,
@@ -171,7 +174,7 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
171
174
  let recommendedUsers;
172
175
  if (fetchOptions && fg('smart-user-picker-load-options-gate')) {
173
176
  recommendedUsers = await fetchOptions(query);
174
- } else if (fg('twcg-444-invite-usd-improvements-m2-gate')) {
177
+ } else {
175
178
  const userRecommendationsPromise = getUserRecommendations(recommendationsRequest, intl);
176
179
  const userResolversPromises = (userResolvers !== null && userResolvers !== void 0 ? userResolvers : []).map(resolver => resolver(query).catch(error => {
177
180
  this.fireEvent(failedUserResolversEvent, {
@@ -182,8 +185,6 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
182
185
  }));
183
186
  const [mainRecommendations, ...userResolverResults] = await Promise.all([userRecommendationsPromise, ...userResolversPromises]);
184
187
  recommendedUsers = [mainRecommendations, ...userResolverResults].flat();
185
- } else {
186
- recommendedUsers = await getUserRecommendations(recommendationsRequest, intl);
187
188
  }
188
189
  if (overrideByline) {
189
190
  for (let option of recommendedUsers) {
@@ -220,6 +221,22 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
220
221
  } else {
221
222
  this.lastEmailSearchFoundMatches = false;
222
223
  }
224
+
225
+ // For a partial query with a suggested domain, check whether the email we would
226
+ // synthesize (`<query>@<domain>`) already belongs to one of the returned users. If so,
227
+ // the real user option is shown instead and the email suggestion is suppressed.
228
+ const {
229
+ suggestEmailsForDomain
230
+ } = this.props;
231
+ if (suggestEmailsForDomain && !isEmail && query) {
232
+ const suggestedEmail = `${query}@${suggestEmailsForDomain}`.toLowerCase();
233
+ this.lastDomainSuggestionMatchedUser = recommendedUsers.some(option => {
234
+ var _option$email;
235
+ return (isUser(option) || isExternalUser(option)) && ((_option$email = option.email) === null || _option$email === void 0 ? void 0 : _option$email.toLowerCase()) === suggestedEmail;
236
+ });
237
+ } else {
238
+ this.lastDomainSuggestionMatchedUser = false;
239
+ }
223
240
  const elapsedTimeMilli = window.performance.now() - startTime;
224
241
  const transformedOptions = transformOptions ? await transformOptions(recommendedUsers, query) : recommendedUsers;
225
242
  const displayedUsers = transformedOptions.length === 0 && onEmpty ? (_await$onEmpty = await onEmpty(query)) !== null && _await$onEmpty !== void 0 ? _await$onEmpty : [] : transformedOptions;
@@ -233,9 +250,9 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
233
250
  displayedUsers: getUsersForAnalytics(displayedUsers),
234
251
  productAttributes,
235
252
  applicable,
236
- ...(fg('twcg-444-invite-usd-improvements-m2-gate') && {
253
+ ...{
237
254
  userResolvers: Array.isArray(userResolvers) ? userResolvers.map(resolver => resolver.name) : []
238
- })
255
+ }
239
256
  });
240
257
  return {
241
258
  users,
@@ -421,20 +438,35 @@ export class SmartUserPickerWithoutAnalytics extends React.Component {
421
438
  allowEmail,
422
439
  enableEmailSearch,
423
440
  allowEmailSelectionWhenEmailMatched,
441
+ suggestEmailsForDomain,
424
442
  ...restProps
425
443
  } = this.props;
426
444
 
427
445
  // Determine whether to allow email selection based on allowEmailSelectionWhenEmailMatched, if needed
428
446
  let shouldAllowEmail = allowEmail;
447
+
448
+ // When suggestEmailsForDomain is active, partial (non-email) input must still reach the base
449
+ // picker's domain-suggestion creatable logic, so the isEmailQuery restriction is relaxed for
450
+ // those queries only. Full email queries that matched an existing user remain suppressed.
451
+ const isDomainSuggestionEnabled = fg('smart_user_picker_suggest_emails_for_domain') && !!suggestEmailsForDomain;
429
452
  if (allowEmail && enableEmailSearch && !allowEmailSelectionWhenEmailMatched) {
430
453
  const isCurrentQueryEmail = isEmailQuery(this.state.query);
431
- // Only allow email selection when:
432
- // 1. The query matches email format (validated by regex)
433
- // 2. No user/external user matches were found (only teams/groups suggested)
434
- shouldAllowEmail = isCurrentQueryEmail && !this.lastEmailSearchFoundMatches;
454
+ if (isDomainSuggestionEnabled) {
455
+ // Allow email for partial queries so the "<query>@<domain>" suggestion can render,
456
+ // but still suppress selection when a full email query matched an existing user,
457
+ // or when the synthesized "<query>@<domain>" is itself an existing user's email.
458
+ shouldAllowEmail = isCurrentQueryEmail ? !this.lastEmailSearchFoundMatches // full email: suppress when user matched
459
+ : !this.lastDomainSuggestionMatchedUser; // partial: suppress when synth email matched a user
460
+ } else {
461
+ // Only allow email selection when:
462
+ // 1. The query matches email format (validated by regex)
463
+ // 2. No user/external user matches were found (only teams/groups suggested)
464
+ shouldAllowEmail = isCurrentQueryEmail && !this.lastEmailSearchFoundMatches;
465
+ }
435
466
  }
436
467
  return /*#__PURE__*/React.createElement(MessagesIntlProvider, null, /*#__PURE__*/React.createElement(UserPicker, _extends({}, restProps, {
437
468
  allowEmail: shouldAllowEmail,
469
+ suggestEmailsForDomain: suggestEmailsForDomain,
438
470
  onInputChange: this.onInputChange,
439
471
  onBlur: this.onBlur,
440
472
  onFocus: this.onFocus,
@@ -6,7 +6,7 @@ import { createAndFireEvent } from '@atlaskit/analytics-next';
6
6
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
7
7
  import { v4 as uuid } from 'uuid';
8
8
  var packageName = "@atlaskit/smart-user-picker";
9
- var packageVersion = "11.2.0";
9
+ var packageVersion = "11.3.1";
10
10
  export var startSession = function startSession() {
11
11
  return {
12
12
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
@@ -9,7 +9,7 @@ import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstruct
9
9
  import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
10
10
  import _inherits from "@babel/runtime/helpers/inherits";
11
11
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
12
- var _excluded = ["allowEmail", "enableEmailSearch", "allowEmailSelectionWhenEmailMatched"];
12
+ var _excluded = ["allowEmail", "enableEmailSearch", "allowEmailSelectionWhenEmailMatched", "suggestEmailsForDomain"];
13
13
  import _regeneratorRuntime from "@babel/runtime/regenerator";
14
14
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
15
15
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
@@ -85,6 +85,9 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
85
85
  });
86
86
  // Track if the last search was an email search that found matches
87
87
  _defineProperty(_this, "lastEmailSearchFoundMatches", false);
88
+ // Track if the domain-suggested email (`<query>@<suggestEmailsForDomain>`) for a partial
89
+ // query already belongs to a returned user, in which case the suggestion is redundant
90
+ _defineProperty(_this, "lastDomainSuggestionMatchedUser", false);
88
91
  _defineProperty(_this, "abortOptionsShownUfoExperience", function () {
89
92
  if (_this.optionsShownUfoExperienceInstance.state.id === UFOExperienceState.STARTED.id) {
90
93
  // There may be an existing UFO timing running from previous key entry or focus,
@@ -110,7 +113,7 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
110
113
  });
111
114
  _defineProperty(_this, "memoizedFilterOptions", memoizeOne(_this.filterOptions));
112
115
  _defineProperty(_this, "getUsers", debounce( /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
113
- var _this$state, query, sessionId, closed, _this$props, baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeTeamsUpdates, includeUsers, includeNonLicensedUsers, intl, fetchOptions, maxOptions, objectId, onEmpty, onError, overrideByline, displayEmailInByline, verifiedTeams, orgId, userbaseId, principalId, productAttributes, productKey, restrictTo, searchQueryFilter, siteId, isTeamSyncedToGroupDirectoryFilter, transformOptions, userResolvers, enableEmailSearch, maxNumberOfResults, startTime, isEmail, recommendationsRequest, _yield$onEmpty, _query, recommendedUsers, userRecommendationsPromise, userResolversPromises, _yield$Promise$all, _yield$Promise$all2, mainRecommendations, userResolverResults, _iterator, _step, option, _iterator2, _step2, _option, _iterator3, _step3, _option2, userMatches, elapsedTimeMilli, transformedOptions, displayedUsers, is5xxEvent, onErrorProducedError, defaultUsers, _elapsedTimeMilli, _t, _t2, _t3, _t4, _t5, _t6, _t7, _t8, _t9;
116
+ var _this$state, query, sessionId, closed, _this$props, baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeTeamsUpdates, includeUsers, includeNonLicensedUsers, intl, fetchOptions, maxOptions, objectId, onEmpty, onError, overrideByline, displayEmailInByline, verifiedTeams, orgId, userbaseId, principalId, productAttributes, productKey, restrictTo, searchQueryFilter, siteId, isTeamSyncedToGroupDirectoryFilter, transformOptions, userResolvers, enableEmailSearch, maxNumberOfResults, startTime, isEmail, recommendationsRequest, _yield$onEmpty, _query, recommendedUsers, userRecommendationsPromise, userResolversPromises, _yield$Promise$all, _yield$Promise$all2, mainRecommendations, userResolverResults, _iterator, _step, option, _iterator2, _step2, _option, _iterator3, _step3, _option2, userMatches, suggestEmailsForDomain, suggestedEmail, elapsedTimeMilli, transformedOptions, displayedUsers, is5xxEvent, onErrorProducedError, defaultUsers, _elapsedTimeMilli, _t, _t2, _t3, _t4, _t5, _t6, _t7, _t8, _t9;
114
117
  return _regeneratorRuntime.wrap(function (_context) {
115
118
  while (1) switch (_context.prev = _context.next) {
116
119
  case 0:
@@ -168,13 +171,9 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
168
171
  return fetchOptions(_query);
169
172
  case 2:
170
173
  recommendedUsers = _context.sent;
171
- _context.next = 7;
174
+ _context.next = 5;
172
175
  break;
173
176
  case 3:
174
- if (!fg('twcg-444-invite-usd-improvements-m2-gate')) {
175
- _context.next = 5;
176
- break;
177
- }
178
177
  userRecommendationsPromise = getUserRecommendations(recommendationsRequest, intl);
179
178
  userResolversPromises = (userResolvers !== null && userResolvers !== void 0 ? userResolvers : []).map(function (resolver) {
180
179
  return resolver(_query).catch(function (error) {
@@ -193,14 +192,7 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
193
192
  mainRecommendations = _yield$Promise$all2[0];
194
193
  userResolverResults = _arrayLikeToArray(_yield$Promise$all2).slice(1);
195
194
  recommendedUsers = [mainRecommendations].concat(_toConsumableArray(userResolverResults)).flat();
196
- _context.next = 7;
197
- break;
198
195
  case 5:
199
- _context.next = 6;
200
- return getUserRecommendations(recommendationsRequest, intl);
201
- case 6:
202
- recommendedUsers = _context.sent;
203
- case 7:
204
196
  if (overrideByline) {
205
197
  _iterator = _createForOfIteratorHelper(recommendedUsers);
206
198
  try {
@@ -262,52 +254,66 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
262
254
  } else {
263
255
  _this.lastEmailSearchFoundMatches = false;
264
256
  }
257
+
258
+ // For a partial query with a suggested domain, check whether the email we would
259
+ // synthesize (`<query>@<domain>`) already belongs to one of the returned users. If so,
260
+ // the real user option is shown instead and the email suggestion is suppressed.
261
+ suggestEmailsForDomain = _this.props.suggestEmailsForDomain;
262
+ if (suggestEmailsForDomain && !isEmail && _query) {
263
+ suggestedEmail = "".concat(_query, "@").concat(suggestEmailsForDomain).toLowerCase();
264
+ _this.lastDomainSuggestionMatchedUser = recommendedUsers.some(function (option) {
265
+ var _option$email;
266
+ return (isUser(option) || isExternalUser(option)) && ((_option$email = option.email) === null || _option$email === void 0 ? void 0 : _option$email.toLowerCase()) === suggestedEmail;
267
+ });
268
+ } else {
269
+ _this.lastDomainSuggestionMatchedUser = false;
270
+ }
265
271
  elapsedTimeMilli = window.performance.now() - startTime;
266
272
  if (!transformOptions) {
267
- _context.next = 9;
273
+ _context.next = 7;
268
274
  break;
269
275
  }
270
- _context.next = 8;
276
+ _context.next = 6;
271
277
  return transformOptions(recommendedUsers, _query);
272
- case 8:
278
+ case 6:
273
279
  _t = _context.sent;
274
- _context.next = 10;
280
+ _context.next = 8;
275
281
  break;
276
- case 9:
282
+ case 7:
277
283
  _t = recommendedUsers;
278
- case 10:
284
+ case 8:
279
285
  transformedOptions = _t;
280
286
  if (!(transformedOptions.length === 0 && onEmpty)) {
281
- _context.next = 15;
287
+ _context.next = 13;
282
288
  break;
283
289
  }
284
- _context.next = 11;
290
+ _context.next = 9;
285
291
  return onEmpty(_query);
286
- case 11:
292
+ case 9:
287
293
  _t4 = _yield$onEmpty = _context.sent;
288
294
  _t3 = _t4 !== null;
289
295
  if (!_t3) {
290
- _context.next = 12;
296
+ _context.next = 10;
291
297
  break;
292
298
  }
293
299
  _t3 = _yield$onEmpty !== void 0;
294
- case 12:
300
+ case 10:
295
301
  if (!_t3) {
296
- _context.next = 13;
302
+ _context.next = 11;
297
303
  break;
298
304
  }
299
305
  _t5 = _yield$onEmpty;
300
- _context.next = 14;
306
+ _context.next = 12;
301
307
  break;
302
- case 13:
308
+ case 11:
303
309
  _t5 = [];
304
- case 14:
310
+ case 12:
305
311
  _t2 = _t5;
306
- _context.next = 16;
312
+ _context.next = 14;
307
313
  break;
308
- case 15:
314
+ case 13:
309
315
  _t2 = transformedOptions;
310
- case 16:
316
+ case 14:
311
317
  displayedUsers = _t2;
312
318
  _this.setState(function (state) {
313
319
  var applicable = state.query === _query;
@@ -319,7 +325,7 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
319
325
  displayedUsers: getUsersForAnalytics(displayedUsers),
320
326
  productAttributes: productAttributes,
321
327
  applicable: applicable
322
- }, fg('twcg-444-invite-usd-improvements-m2-gate') && {
328
+ }, {
323
329
  userResolvers: Array.isArray(userResolvers) ? userResolvers.map(function (resolver) {
324
330
  return resolver.name;
325
331
  }) : []
@@ -329,10 +335,10 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
329
335
  loading: loading
330
336
  };
331
337
  });
332
- _context.next = 25;
338
+ _context.next = 23;
333
339
  break;
334
- case 17:
335
- _context.prev = 17;
340
+ case 15:
341
+ _context.prev = 15;
336
342
  _t6 = _context["catch"](1);
337
343
  is5xxEvent = checkIf500Event(_t6.statusCode);
338
344
  if (!closed && !onError && is5xxEvent) {
@@ -345,35 +351,35 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
345
351
  });
346
352
  onErrorProducedError = false;
347
353
  defaultUsers = [];
348
- _context.prev = 18;
354
+ _context.prev = 16;
349
355
  if (!onError) {
350
- _context.next = 21;
356
+ _context.next = 19;
351
357
  break;
352
358
  }
353
- _context.next = 19;
359
+ _context.next = 17;
354
360
  return onError(_t6, recommendationsRequest);
355
- case 19:
361
+ case 17:
356
362
  _t8 = _context.sent;
357
363
  if (_t8) {
358
- _context.next = 20;
364
+ _context.next = 18;
359
365
  break;
360
366
  }
361
367
  _t8 = [];
362
- case 20:
368
+ case 18:
363
369
  _t7 = _t8;
364
- _context.next = 22;
370
+ _context.next = 20;
365
371
  break;
366
- case 21:
372
+ case 19:
367
373
  _t7 = [];
368
- case 22:
374
+ case 20:
369
375
  defaultUsers = _t7;
370
- _context.next = 24;
376
+ _context.next = 22;
371
377
  break;
372
- case 23:
373
- _context.prev = 23;
374
- _t9 = _context["catch"](18);
378
+ case 21:
379
+ _context.prev = 21;
380
+ _t9 = _context["catch"](16);
375
381
  onErrorProducedError = true;
376
- case 24:
382
+ case 22:
377
383
  if (onErrorProducedError && is5xxEvent) {
378
384
  // Log error from fallback data source `onError` to UFO
379
385
  _this.optionsShownUfoExperienceInstance.failure(ufoEndStateConfig(_this.props.fieldId));
@@ -390,11 +396,11 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
390
396
  elapsedTimeMilli: _elapsedTimeMilli,
391
397
  productAttributes: productAttributes
392
398
  });
393
- case 25:
399
+ case 23:
394
400
  case "end":
395
401
  return _context.stop();
396
402
  }
397
- }, _callee, null, [[1, 17], [18, 23]]);
403
+ }, _callee, null, [[1, 15], [16, 21]]);
398
404
  })), (_this$props$debounceT = _this.props.debounceTime) !== null && _this$props$debounceT !== void 0 ? _this$props$debounceT : 0));
399
405
  _defineProperty(_this, "onInputChange", function (newQuery, sessionId) {
400
406
  var query = newQuery || '';
@@ -569,19 +575,34 @@ export var SmartUserPickerWithoutAnalytics = /*#__PURE__*/function (_React$Compo
569
575
  allowEmail = _this$props2.allowEmail,
570
576
  enableEmailSearch = _this$props2.enableEmailSearch,
571
577
  allowEmailSelectionWhenEmailMatched = _this$props2.allowEmailSelectionWhenEmailMatched,
578
+ suggestEmailsForDomain = _this$props2.suggestEmailsForDomain,
572
579
  restProps = _objectWithoutProperties(_this$props2, _excluded);
573
580
 
574
581
  // Determine whether to allow email selection based on allowEmailSelectionWhenEmailMatched, if needed
575
582
  var shouldAllowEmail = allowEmail;
583
+
584
+ // When suggestEmailsForDomain is active, partial (non-email) input must still reach the base
585
+ // picker's domain-suggestion creatable logic, so the isEmailQuery restriction is relaxed for
586
+ // those queries only. Full email queries that matched an existing user remain suppressed.
587
+ var isDomainSuggestionEnabled = fg('smart_user_picker_suggest_emails_for_domain') && !!suggestEmailsForDomain;
576
588
  if (allowEmail && enableEmailSearch && !allowEmailSelectionWhenEmailMatched) {
577
589
  var isCurrentQueryEmail = isEmailQuery(this.state.query);
578
- // Only allow email selection when:
579
- // 1. The query matches email format (validated by regex)
580
- // 2. No user/external user matches were found (only teams/groups suggested)
581
- shouldAllowEmail = isCurrentQueryEmail && !this.lastEmailSearchFoundMatches;
590
+ if (isDomainSuggestionEnabled) {
591
+ // Allow email for partial queries so the "<query>@<domain>" suggestion can render,
592
+ // but still suppress selection when a full email query matched an existing user,
593
+ // or when the synthesized "<query>@<domain>" is itself an existing user's email.
594
+ shouldAllowEmail = isCurrentQueryEmail ? !this.lastEmailSearchFoundMatches // full email: suppress when user matched
595
+ : !this.lastDomainSuggestionMatchedUser; // partial: suppress when synth email matched a user
596
+ } else {
597
+ // Only allow email selection when:
598
+ // 1. The query matches email format (validated by regex)
599
+ // 2. No user/external user matches were found (only teams/groups suggested)
600
+ shouldAllowEmail = isCurrentQueryEmail && !this.lastEmailSearchFoundMatches;
601
+ }
582
602
  }
583
603
  return /*#__PURE__*/React.createElement(MessagesIntlProvider, null, /*#__PURE__*/React.createElement(UserPicker, _extends({}, restProps, {
584
604
  allowEmail: shouldAllowEmail,
605
+ suggestEmailsForDomain: suggestEmailsForDomain,
585
606
  onInputChange: this.onInputChange,
586
607
  onBlur: this.onBlur,
587
608
  onFocus: this.onFocus,
@@ -8,6 +8,7 @@ import type { DebouncedFunc } from 'lodash';
8
8
  export declare class SmartUserPickerWithoutAnalytics extends React.Component<Props & WrappedComponentProps, State> {
9
9
  state: State;
10
10
  private lastEmailSearchFoundMatches;
11
+ private lastDomainSuggestionMatchedUser;
11
12
  optionsShownUfoExperienceInstance: UFOExperience;
12
13
  static defaultProps: {
13
14
  baseUrl: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-user-picker",
3
- "version": "11.3.0",
3
+ "version": "11.3.2",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -44,7 +44,7 @@
44
44
  "devDependencies": {
45
45
  "@atlaskit/select": "^22.6.0",
46
46
  "@atlaskit/util-data-test": "^19.1.0",
47
- "@atlassian/a11y-jest-testing": "^0.13.0",
47
+ "@atlassian/a11y-jest-testing": "^0.14.0",
48
48
  "@testing-library/dom": "^10.1.0",
49
49
  "@testing-library/react": "^16.3.0",
50
50
  "@testing-library/user-event": "^14.4.3",
@@ -57,7 +57,7 @@
57
57
  "react-intl": "^7.0.0"
58
58
  },
59
59
  "platform-feature-flags": {
60
- "twcg-444-invite-usd-improvements-m2-gate": {
60
+ "smart_user_picker_suggest_emails_for_domain": {
61
61
  "type": "boolean"
62
62
  },
63
63
  "smart-user-picker-load-options-gate": {