@atlaskit/link-create 1.10.0 → 1.11.0

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/analytics.spec.yaml +29 -3
  3. package/dist/cjs/common/messages.js +14 -0
  4. package/dist/cjs/common/ui/error-boundary-ui/index.js +4 -3
  5. package/dist/cjs/common/ui/error-boundary-ui/messages.js +0 -5
  6. package/dist/cjs/common/utils/analytics/analytics.codegen.js +1 -1
  7. package/dist/cjs/common/utils/errors/index.js +39 -0
  8. package/dist/cjs/controllers/callback-context/main.js +23 -7
  9. package/dist/cjs/controllers/experience-tracker/index.js +85 -0
  10. package/dist/cjs/index.js +7 -0
  11. package/dist/cjs/ui/create-form/async-select/main.js +62 -20
  12. package/dist/cjs/ui/create-form/main.js +62 -7
  13. package/dist/cjs/ui/link-create/error-boundary/index.js +14 -9
  14. package/dist/cjs/ui/link-create/main.js +5 -3
  15. package/dist/cjs/ui/main.js +9 -9
  16. package/dist/es2019/common/messages.js +8 -0
  17. package/dist/es2019/common/ui/error-boundary-ui/index.js +2 -1
  18. package/dist/es2019/common/ui/error-boundary-ui/messages.js +0 -5
  19. package/dist/es2019/common/utils/analytics/analytics.codegen.js +1 -1
  20. package/dist/es2019/common/utils/errors/index.js +31 -0
  21. package/dist/es2019/controllers/callback-context/main.js +19 -3
  22. package/dist/es2019/controllers/experience-tracker/index.js +75 -0
  23. package/dist/es2019/index.js +1 -0
  24. package/dist/es2019/ui/create-form/async-select/main.js +32 -1
  25. package/dist/es2019/ui/create-form/main.js +47 -3
  26. package/dist/es2019/ui/link-create/error-boundary/index.js +14 -9
  27. package/dist/es2019/ui/link-create/main.js +5 -3
  28. package/dist/es2019/ui/main.js +10 -9
  29. package/dist/esm/common/messages.js +8 -0
  30. package/dist/esm/common/ui/error-boundary-ui/index.js +2 -1
  31. package/dist/esm/common/ui/error-boundary-ui/messages.js +0 -5
  32. package/dist/esm/common/utils/analytics/analytics.codegen.js +1 -1
  33. package/dist/esm/common/utils/errors/index.js +32 -0
  34. package/dist/esm/controllers/callback-context/main.js +23 -7
  35. package/dist/esm/controllers/experience-tracker/index.js +75 -0
  36. package/dist/esm/index.js +1 -0
  37. package/dist/esm/ui/create-form/async-select/main.js +62 -20
  38. package/dist/esm/ui/create-form/main.js +62 -7
  39. package/dist/esm/ui/link-create/error-boundary/index.js +14 -9
  40. package/dist/esm/ui/link-create/main.js +5 -3
  41. package/dist/esm/ui/main.js +10 -10
  42. package/dist/types/common/messages.d.ts +8 -0
  43. package/dist/types/common/ui/error-boundary-ui/messages.d.ts +0 -5
  44. package/dist/types/common/utils/analytics/analytics.codegen.d.ts +11 -2
  45. package/dist/types/common/utils/errors/index.d.ts +10 -0
  46. package/dist/types/controllers/callback-context/main.d.ts +1 -1
  47. package/dist/types/controllers/experience-tracker/index.d.ts +22 -0
  48. package/dist/types/index.d.ts +1 -0
  49. package/dist/types/ui/create-form/async-select/main.d.ts +1 -1
  50. package/dist/types/ui/create-form/main.d.ts +18 -1
  51. package/dist/types-ts4.5/common/messages.d.ts +8 -0
  52. package/dist/types-ts4.5/common/ui/error-boundary-ui/messages.d.ts +0 -5
  53. package/dist/types-ts4.5/common/utils/analytics/analytics.codegen.d.ts +11 -2
  54. package/dist/types-ts4.5/common/utils/errors/index.d.ts +10 -0
  55. package/dist/types-ts4.5/controllers/callback-context/main.d.ts +1 -1
  56. package/dist/types-ts4.5/controllers/experience-tracker/index.d.ts +22 -0
  57. package/dist/types-ts4.5/index.d.ts +1 -0
  58. package/dist/types-ts4.5/ui/create-form/async-select/main.d.ts +1 -1
  59. package/dist/types-ts4.5/ui/create-form/main.d.ts +18 -1
  60. package/package.json +8 -4
  61. package/report.api.md +11 -8
  62. package/tmp/api-report-tmp.d.ts +9 -8
@@ -7,19 +7,24 @@ Object.defineProperty(exports, "__esModule", {
7
7
  });
8
8
  exports.TEST_ID = exports.CreateForm = void 0;
9
9
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
10
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
10
11
  var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
11
12
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
12
13
  var _react = require("react");
13
14
  var _react2 = require("@emotion/react");
15
+ var _finalForm = require("final-form");
14
16
  var _reactFinalForm = require("react-final-form");
17
+ var _reactIntlNext = require("react-intl-next");
15
18
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
16
19
  var _primitives = require("@atlaskit/primitives");
17
20
  var _constants = require("../../common/constants");
21
+ var _messages = _interopRequireDefault(require("../../common/messages"));
22
+ var _callbackContext = require("../../controllers/callback-context");
18
23
  var _exitWarningModalContext = require("../../controllers/exit-warning-modal-context");
19
24
  var _formContext = require("../../controllers/form-context");
20
25
  var _formFooter = require("./form-footer");
21
26
  var _formLoader = require("./form-loader");
22
- var _excluded = ["submitting"];
27
+ var _excluded = ["submitting", "submitError"];
23
28
  function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
24
29
  function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** @jsx jsx */
25
30
  var formStyles = (0, _react2.css)({
@@ -39,10 +44,14 @@ var CreateForm = exports.CreateForm = function CreateForm(_ref) {
39
44
  hideFooter = _ref.hideFooter,
40
45
  initialValues = _ref.initialValues;
41
46
  var _useFormContext = (0, _formContext.useFormContext)(),
47
+ setFormErrorMessage = _useFormContext.setFormErrorMessage,
42
48
  formErrorMessage = _useFormContext.formErrorMessage,
43
49
  enableEditView = _useFormContext.enableEditView;
50
+ var intl = (0, _reactIntlNext.useIntl)();
44
51
  var _useExitWarningModal = (0, _exitWarningModalContext.useExitWarningModal)(),
45
52
  setShouldShowWarning = _useExitWarningModal.setShouldShowWarning;
53
+ var _useLinkCreateCallbac = (0, _callbackContext.useLinkCreateCallback)(),
54
+ onFailure = _useLinkCreateCallbac.onFailure;
46
55
  var handleSubmit = (0, _react.useCallback)( /*#__PURE__*/function () {
47
56
  var _ref2 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(data) {
48
57
  var shouldEnableEditView, formData;
@@ -60,6 +69,11 @@ var CreateForm = exports.CreateForm = function CreateForm(_ref) {
60
69
  * if submission is successful
61
70
  */
62
71
  enableEditView === null || enableEditView === void 0 || enableEditView(!!shouldEnableEditView);
72
+
73
+ /**
74
+ * This is the onSubmit handler provided by the plugin
75
+ * It will be async, and it will likely involve awaiting `onCreate` (the adopters handler)
76
+ */
63
77
  return _context.abrupt("return", onSubmit(formData));
64
78
  case 4:
65
79
  return _context.abrupt("return", onSubmit(data));
@@ -73,6 +87,40 @@ var CreateForm = exports.CreateForm = function CreateForm(_ref) {
73
87
  return _ref2.apply(this, arguments);
74
88
  };
75
89
  }(), [onSubmit, enableEditView]);
90
+ var handleSubmitWithErrorHandling = (0, _react.useCallback)( /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2() {
91
+ var _args2 = arguments;
92
+ return _regenerator.default.wrap(function _callee2$(_context2) {
93
+ while (1) switch (_context2.prev = _context2.next) {
94
+ case 0:
95
+ _context2.prev = 0;
96
+ /**
97
+ * Clear any error message that may have been set by async select fields
98
+ * This will immediately remove any indication of an error, but the form likely will fail to submit,
99
+ * it will be likely a 400 because the user probably could not set all fields anyway
100
+ */
101
+ setFormErrorMessage();
102
+ _context2.next = 4;
103
+ return handleSubmit.apply(void 0, _args2);
104
+ case 4:
105
+ return _context2.abrupt("return", _context2.sent);
106
+ case 7:
107
+ _context2.prev = 7;
108
+ _context2.t0 = _context2["catch"](0);
109
+ /**
110
+ * Notify link create of failed experience
111
+ */
112
+ onFailure === null || onFailure === void 0 || onFailure(_context2.t0);
113
+
114
+ /**
115
+ * Return a generic message for react final form to render
116
+ */
117
+ return _context2.abrupt("return", (0, _defineProperty2.default)({}, _finalForm.FORM_ERROR, intl.formatMessage(_messages.default.genericErrorMessage)));
118
+ case 11:
119
+ case "end":
120
+ return _context2.stop();
121
+ }
122
+ }, _callee2, null, [[0, 7]]);
123
+ })), [handleSubmit, setFormErrorMessage, intl, onFailure]);
76
124
  var handleCancel = (0, _react.useCallback)(function () {
77
125
  onCancel && onCancel();
78
126
  }, [onCancel]);
@@ -80,7 +128,7 @@ var CreateForm = exports.CreateForm = function CreateForm(_ref) {
80
128
  return (0, _react2.jsx)(_formLoader.CreateFormLoader, null);
81
129
  }
82
130
  return (0, _react2.jsx)(_reactFinalForm.Form, {
83
- onSubmit: handleSubmit,
131
+ onSubmit: (0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.better-observability') ? handleSubmitWithErrorHandling : handleSubmit,
84
132
  initialValues: initialValues,
85
133
  mutators: {
86
134
  setField: function setField(args, state, tools) {
@@ -89,9 +137,10 @@ var CreateForm = exports.CreateForm = function CreateForm(_ref) {
89
137
  });
90
138
  }
91
139
  }
92
- }, function (_ref3) {
93
- var submitting = _ref3.submitting,
94
- formProps = (0, _objectWithoutProperties2.default)(_ref3, _excluded);
140
+ }, function (_ref5) {
141
+ var submitting = _ref5.submitting,
142
+ submitError = _ref5.submitError,
143
+ formProps = (0, _objectWithoutProperties2.default)(_ref5, _excluded);
95
144
  return (0, _react2.jsx)("form", {
96
145
  onSubmit: formProps.handleSubmit,
97
146
  name: "link-create-form",
@@ -112,8 +161,14 @@ var CreateForm = exports.CreateForm = function CreateForm(_ref) {
112
161
  });
113
162
  setShouldShowWarning(isModified);
114
163
  }
115
- }), (0, _react2.jsx)(_primitives.Box, null, children), !hideFooter && (0, _react2.jsx)(_formFooter.CreateFormFooter, {
116
- formErrorMessage: formErrorMessage,
164
+ }), (0, _react2.jsx)(_primitives.Box, null, children), !hideFooter && (0, _react2.jsx)(_formFooter.CreateFormFooter
165
+ /**
166
+ * We will prefer to render the error message connected to
167
+ * react final form state (submitError) otherwise we can
168
+ * default to the `formErrorMessage` that we sometimes use with our own
169
+ * "form context" (only currently used for AsyncSelect field reporting failed loading)
170
+ */, {
171
+ formErrorMessage: (0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.better-observability') ? submitError || formErrorMessage : formErrorMessage,
117
172
  handleCancel: handleCancel,
118
173
  submitting: submitting,
119
174
  testId: testId
@@ -13,6 +13,7 @@ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
13
13
  var _constants = require("../../../common/constants");
14
14
  var _errorBoundaryUi = require("../../../common/ui/error-boundary-ui");
15
15
  var _analytics = _interopRequireDefault(require("../../../common/utils/analytics/analytics.codegen"));
16
+ var _experienceTracker = require("../../../controllers/experience-tracker");
16
17
  var _errorBoundaryBase = require("./error-boundary-base");
17
18
  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); }
18
19
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
@@ -21,14 +22,17 @@ var ErrorBoundary = exports.ErrorBoundary = function ErrorBoundary(_ref) {
21
22
  errorComponent = _ref.errorComponent;
22
23
  var _useAnalyticsEvents = (0, _analyticsNext.useAnalyticsEvents)(),
23
24
  createAnalyticsEvent = _useAnalyticsEvents.createAnalyticsEvent;
25
+ var experience = (0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.better-observability') ?
26
+ // eslint-disable-next-line react-hooks/rules-of-hooks
27
+ (0, _experienceTracker.useExperience)() : null;
24
28
  var handleError = (0, _react.useCallback)(function (error, info) {
25
29
  var _window, _window2, _info$componentStack;
26
- if ((0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.enable-sentry-client')) {
27
- // Capture exception to Sentry
28
- (0, _sentry.captureException)(error, 'link-create');
30
+ if (!(0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.better-observability')) {
31
+ if ((0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.enable-sentry-client')) {
32
+ // Capture exception to Sentry
33
+ (0, _sentry.captureException)(error, 'link-create');
34
+ }
29
35
  }
30
-
31
- // Fire Analytics event
32
36
  createAnalyticsEvent((0, _analytics.default)('operational.linkCreate.unhandledErrorCaught', (0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.enable-sentry-client') ? {
33
37
  browserInfo: ((_window = window) === null || _window === void 0 || (_window = _window.navigator) === null || _window === void 0 ? void 0 : _window.userAgent) || 'unknown',
34
38
  error: error.name,
@@ -38,10 +42,11 @@ var ErrorBoundary = exports.ErrorBoundary = function ErrorBoundary(_ref) {
38
42
  error: error.toString(),
39
43
  componentStack: (_info$componentStack = info === null || info === void 0 ? void 0 : info.componentStack) !== null && _info$componentStack !== void 0 ? _info$componentStack : ''
40
44
  })).fire(_constants.ANALYTICS_CHANNEL);
41
-
42
- // Fire UFO failed experience
43
- // failUfoExperience(ufoExperience.mounted);
44
- }, [createAnalyticsEvent]);
45
+ if ((0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.better-observability')) {
46
+ // Track experience as failed for SLO
47
+ experience === null || experience === void 0 || experience.failure(error);
48
+ }
49
+ }, [createAnalyticsEvent, experience]);
45
50
  return /*#__PURE__*/_react.default.createElement(_errorBoundaryBase.BaseErrorBoundary, {
46
51
  onError: handleError,
47
52
  errorComponent: errorComponent !== null && errorComponent !== void 0 ? errorComponent : /*#__PURE__*/_react.default.createElement(_errorBoundaryUi.ErrorBoundaryUI, null)
@@ -82,7 +82,9 @@ var LinkCreateWithModal = function LinkCreateWithModal(_ref2) {
82
82
  }(), [onCreate, setFormErrorMessage]);
83
83
  var handleFailure = (0, _react.useCallback)(function (error) {
84
84
  // Set the form error message
85
- setFormErrorMessage(error.message);
85
+ if (error instanceof Error) {
86
+ setFormErrorMessage(error.message);
87
+ }
86
88
  onFailure && onFailure(error);
87
89
  }, [onFailure, setFormErrorMessage]);
88
90
  var _useExitWarningModal = (0, _exitWarningModalContext.useExitWarningModal)(),
@@ -104,8 +106,8 @@ var LinkCreateWithModal = function LinkCreateWithModal(_ref2) {
104
106
  return setShowExitWarning(false);
105
107
  }, []);
106
108
  return (0, _react2.jsx)(_callbackContext.LinkCreateCallbackProvider, {
107
- onCreate: handleCreate,
108
- onFailure: handleFailure,
109
+ onCreate: (0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.better-observability') ? onCreate : handleCreate,
110
+ onFailure: (0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.better-observability') ? onFailure : handleFailure,
109
111
  onCancel: handleCancel
110
112
  }, (0, _react2.jsx)(_modalDialog.ModalTransition, null, active && (0, _react2.jsx)(_ModalDialog.Modal, {
111
113
  testId: "link-create-modal",
@@ -5,35 +5,35 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.default = exports.PACKAGE_DATA = void 0;
8
- var _objectDestructuringEmpty2 = _interopRequireDefault(require("@babel/runtime/helpers/objectDestructuringEmpty"));
9
- var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
10
8
  var _react = require("react");
11
9
  var _react2 = require("@emotion/react");
12
10
  var _analyticsNext = require("@atlaskit/analytics-next");
13
11
  var _intlMessagesProvider = require("@atlaskit/intl-messages-provider");
12
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
14
13
  var _constants = require("../common/constants");
15
14
  var _errorBoundaryModal = require("../common/ui/error-boundary-modal");
16
15
  var _analytics = require("../common/utils/analytics");
17
16
  var _fetchMessagesForLocale = require("../common/utils/locale/fetch-messages-for-locale");
17
+ var _experienceTracker = require("../controllers/experience-tracker");
18
18
  var _en = _interopRequireDefault(require("../i18n/en"));
19
19
  var _linkCreate = _interopRequireDefault(require("./link-create"));
20
20
  var _errorBoundary = require("./link-create/error-boundary");
21
21
  /** @jsx jsx */
22
22
 
23
- var LinkCreateWithAnalyticsContext = (0, _analytics.withLinkCreateAnalyticsContext)( /*#__PURE__*/(0, _react.memo)(function (_ref) {
24
- var props = (0, _extends2.default)({}, ((0, _objectDestructuringEmpty2.default)(_ref), _ref));
25
- return (0, _react2.jsx)(_errorBoundary.ErrorBoundary, {
23
+ var LinkCreateWithAnalyticsContext = (0, _analytics.withLinkCreateAnalyticsContext)( /*#__PURE__*/(0, _react.memo)(function (props) {
24
+ var ExperienceProvider = (0, _platformFeatureFlags.getBooleanFF)('platform.linking-platform.link-create.better-observability') ? _experienceTracker.Experience : _react.Fragment;
25
+ return (0, _react2.jsx)(ExperienceProvider, null, (0, _react2.jsx)(_errorBoundary.ErrorBoundary, {
26
26
  errorComponent: (0, _react2.jsx)(_errorBoundaryModal.ErrorBoundaryModal, {
27
27
  active: props.active,
28
28
  onClose: props.onCancel
29
29
  })
30
- }, (0, _react2.jsx)(_linkCreate.default, props));
30
+ }, (0, _react2.jsx)(_linkCreate.default, props)));
31
31
  }));
32
32
  var PACKAGE_DATA = exports.PACKAGE_DATA = {
33
33
  packageName: "@atlaskit/link-create" || '',
34
- packageVersion: "1.10.0" || '',
35
- componentName: _constants.COMPONENT_NAME,
36
- source: _constants.COMPONENT_NAME
34
+ packageVersion: "1.11.0" || '',
35
+ component: _constants.COMPONENT_NAME,
36
+ componentName: _constants.COMPONENT_NAME
37
37
  };
38
38
  var ComposedLinkCreate = /*#__PURE__*/(0, _react.memo)(function (props) {
39
39
  return (0, _react2.jsx)(_analyticsNext.AnalyticsContext, {
@@ -0,0 +1,8 @@
1
+ import { defineMessages } from 'react-intl-next';
2
+ export default defineMessages({
3
+ genericErrorMessage: {
4
+ id: 'link-create.unknown-error.heading',
5
+ defaultMessage: 'Something went wrong',
6
+ description: 'Message when an unknown error occurs'
7
+ }
8
+ });
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { FormattedMessage, useIntl } from 'react-intl-next';
4
4
  import Button from '@atlaskit/button';
5
5
  import EmptyState from '@atlaskit/empty-state';
6
+ import commonMessages from '../../messages';
6
7
  import ErrorSVG from './error-svg';
7
8
  import messages from './messages';
8
9
  export const CONTACT_SUPPORT_LINK = 'https://support.atlassian.com/contact/';
@@ -11,7 +12,7 @@ export const ErrorBoundaryUI = () => {
11
12
  return /*#__PURE__*/React.createElement(EmptyState, {
12
13
  maxImageWidth: 82,
13
14
  testId: 'link-create-error-boundary-ui',
14
- header: intl.formatMessage(messages.heading),
15
+ header: intl.formatMessage(commonMessages.genericErrorMessage),
15
16
  description: /*#__PURE__*/React.createElement(FormattedMessage, _extends({}, messages.description, {
16
17
  values: {
17
18
  a: label => /*#__PURE__*/React.createElement(Button, {
@@ -1,10 +1,5 @@
1
1
  import { defineMessages } from 'react-intl-next';
2
2
  export default defineMessages({
3
- heading: {
4
- id: 'link-create.unknown-error.heading',
5
- defaultMessage: 'Something went wrong',
6
- description: 'Heading when an unknown error occurs'
7
- },
8
3
  description: {
9
4
  id: 'link-create.unknown-error.description',
10
5
  defaultMessage: 'Refresh the page, or contact <a>Atlassian Support</a> if this keeps happening.',
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Generates Typescript types for analytics events from analytics.spec.yaml
5
5
  *
6
- * @codegen <<SignedSource::be704bbbca0e49c927d1268b9a0f1d6a>>
6
+ * @codegen <<SignedSource::20562f2db603daab7d5b7a5c5ba1c04b>>
7
7
  * @codegenCommand yarn workspace @atlaskit/link-create run codegen-analytics
8
8
  */
9
9
 
@@ -0,0 +1,31 @@
1
+ import { getTraceId } from '@atlaskit/linking-common/utils';
2
+ const getUrlPath = url => {
3
+ try {
4
+ return new URL(url).pathname;
5
+ } catch {
6
+ return 'Failed to parse pathname from url';
7
+ }
8
+ };
9
+ export const getNetworkFields = error => {
10
+ if (error instanceof Response) {
11
+ return {
12
+ traceId: getTraceId(error),
13
+ status: error.status,
14
+ path: getUrlPath(error.url)
15
+ };
16
+ }
17
+ return {
18
+ traceId: null,
19
+ status: null,
20
+ path: null
21
+ };
22
+ };
23
+ export const getErrorType = error => {
24
+ if (error instanceof Response) {
25
+ return 'NetworkError';
26
+ }
27
+ if (error instanceof Error) {
28
+ return error.name;
29
+ }
30
+ return typeof error;
31
+ };
@@ -1,7 +1,10 @@
1
1
  import React, { useContext, useMemo } from 'react';
2
2
  import { useAnalyticsEvents } from '@atlaskit/analytics-next';
3
+ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
3
4
  import { ANALYTICS_CHANNEL } from '../../common/constants';
4
5
  import createEventPayload from '../../common/utils/analytics/analytics.codegen';
6
+ import { getErrorType } from '../../common/utils/errors';
7
+ import { useExperience } from '../experience-tracker';
5
8
  const LinkCreateCallbackContext = /*#__PURE__*/React.createContext({});
6
9
  const LinkCreateCallbackProvider = ({
7
10
  children,
@@ -12,8 +15,18 @@ const LinkCreateCallbackProvider = ({
12
15
  const {
13
16
  createAnalyticsEvent
14
17
  } = useAnalyticsEvents();
18
+ const experience = getBooleanFF('platform.linking-platform.link-create.better-observability') ?
19
+ // eslint-disable-next-line react-hooks/rules-of-hooks
20
+ useExperience() : null;
15
21
  const handleCreate = useMemo(() => ({
16
22
  onCreate: async result => {
23
+ if (getBooleanFF('platform.linking-platform.link-create.better-observability')) {
24
+ /**
25
+ * We consider the experience successful once we have
26
+ * successfully created an object
27
+ */
28
+ experience === null || experience === void 0 ? void 0 : experience.success();
29
+ }
17
30
  const {
18
31
  objectId,
19
32
  objectType
@@ -26,15 +39,18 @@ const LinkCreateCallbackProvider = ({
26
39
  await onCreate(result);
27
40
  }
28
41
  }
29
- }), [createAnalyticsEvent, onCreate]);
42
+ }), [createAnalyticsEvent, onCreate, experience]);
30
43
  const handleFailure = useMemo(() => ({
31
44
  onFailure: async error => {
32
45
  createAnalyticsEvent(createEventPayload('track.object.createFailed.linkCreate', {
33
- failureType: error.name
46
+ failureType: getErrorType(error)
34
47
  })).fire(ANALYTICS_CHANNEL);
48
+ if (getBooleanFF('platform.linking-platform.link-create.better-observability')) {
49
+ experience === null || experience === void 0 ? void 0 : experience.failure(error);
50
+ }
35
51
  onFailure && onFailure(error);
36
52
  }
37
- }), [createAnalyticsEvent, onFailure]);
53
+ }), [createAnalyticsEvent, onFailure, experience]);
38
54
  const value = useMemo(() => ({
39
55
  onCancel,
40
56
  ...handleCreate,
@@ -0,0 +1,75 @@
1
+ import React, { createContext, useContext, useMemo, useRef } from 'react';
2
+ import { useAnalyticsEvents } from '@atlaskit/analytics-next';
3
+ import { captureException } from '@atlaskit/linking-common/sentry';
4
+ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
5
+ import { ANALYTICS_CHANNEL } from '../../common/constants';
6
+ import createEventPayload from '../../common/utils/analytics/analytics.codegen';
7
+ import { getErrorType, getNetworkFields } from '../../common/utils/errors';
8
+ const ExperienceContext = /*#__PURE__*/createContext({
9
+ success: () => {},
10
+ failure: () => {}
11
+ });
12
+
13
+ /**
14
+ * Experience provider that simply keeps track of the state of the experience.
15
+ * Fires an operational event when experience state changes to FAILED.
16
+ */
17
+ export const Experience = ({
18
+ children
19
+ }) => {
20
+ const {
21
+ createAnalyticsEvent
22
+ } = useAnalyticsEvents();
23
+ const experience = useRef('STARTED');
24
+ const value = useMemo(() => ({
25
+ success: () => {
26
+ if (experience.current !== 'SUCCEEDED') {
27
+ experience.current = 'SUCCEEDED';
28
+ }
29
+ },
30
+ /**
31
+ * Indicate the experience has failed and capture exception information
32
+ * @param error Typically an Error class or Response class
33
+ */
34
+ failure: error => {
35
+ const experienceStatus = 'FAILED';
36
+
37
+ /**
38
+ * Always capture an event to Splunk
39
+ */
40
+ createAnalyticsEvent(createEventPayload('operational.operation.failed.linkCreate', {
41
+ /**
42
+ * The type of error that has failed the experience
43
+ */
44
+ errorType: getErrorType(error),
45
+ /**
46
+ * The current status of the experience (has failed)
47
+ */
48
+ experienceStatus,
49
+ /**
50
+ * Previous experience status indicates whether the experience
51
+ * has just failed now, or has already failing
52
+ */
53
+ previousExperienceStatus: experience.current,
54
+ /**
55
+ * Fields related to `Response` object that can help with debugging
56
+ * what has gone wrong
57
+ */
58
+ ...getNetworkFields(error)
59
+ })).fire(ANALYTICS_CHANNEL);
60
+ if (error instanceof Error) {
61
+ if (getBooleanFF('platform.linking-platform.link-create.enable-sentry-client')) {
62
+ // Capture exception to Sentry
63
+ captureException(error, 'link-create');
64
+ }
65
+ }
66
+ if (experience.current !== experienceStatus) {
67
+ experience.current = experienceStatus;
68
+ }
69
+ }
70
+ }), [experience, createAnalyticsEvent]);
71
+ return /*#__PURE__*/React.createElement(ExperienceContext.Provider, {
72
+ value: value
73
+ }, children);
74
+ };
75
+ export const useExperience = () => useContext(ExperienceContext);
@@ -1,2 +1,3 @@
1
+ export { FORM_ERROR } from 'final-form';
1
2
  export { default, TextField, CreateForm, Select, AsyncSelect, CreateFormLoader, FormSpy } from './ui/index';
2
3
  export { useLinkCreateCallback, LinkCreateCallbackProvider } from './controllers/callback-context';
@@ -4,8 +4,13 @@ import { useEffect, useMemo, useState } from 'react';
4
4
  import { jsx } from '@emotion/react';
5
5
  import debounce from 'debounce-promise';
6
6
  import { useForm } from 'react-final-form';
7
+ import { useIntl } from 'react-intl-next';
8
+ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
7
9
  import { AsyncSelect as AkAsyncSelect } from '@atlaskit/select';
10
+ import messages from '../../../common/messages';
11
+ import { useLinkCreateCallback } from '../../../controllers/callback-context';
8
12
  import { CreateField } from '../../../controllers/create-field';
13
+ import { useFormContext } from '../../../controllers/form-context';
9
14
  export const TEST_ID = 'link-create-async-select';
10
15
 
11
16
  /**
@@ -23,15 +28,41 @@ export function AsyncSelect({
23
28
  validationHelpText,
24
29
  testId = TEST_ID,
25
30
  defaultOption: propsDefaultValue,
26
- loadOptions,
31
+ loadOptions: loadOptionsFn,
27
32
  ...restProps
28
33
  }) {
29
34
  const {
30
35
  mutators
31
36
  } = useForm();
37
+ const {
38
+ onFailure
39
+ } = useLinkCreateCallback();
40
+ const {
41
+ setFormErrorMessage
42
+ } = useFormContext();
43
+ const intl = useIntl();
32
44
  const [defaultValue, setDefaultValue] = useState(propsDefaultValue);
33
45
  const [isLoadingDefaultOptions, setIsLoadingDefaultOptions] = useState(false);
34
46
  const [defaultOptions, setDefaultOptions] = useState([]);
47
+
48
+ /**
49
+ * This binds experience to fail if async fetch ever fails to load
50
+ */
51
+ const loadOptions = getBooleanFF('platform.linking-platform.link-create.better-observability') ?
52
+ // eslint-disable-next-line react-hooks/rules-of-hooks
53
+ useMemo(() => {
54
+ if (loadOptionsFn) {
55
+ return async function (...args) {
56
+ try {
57
+ return await loadOptionsFn(...args);
58
+ } catch (err) {
59
+ onFailure === null || onFailure === void 0 ? void 0 : onFailure(err);
60
+ setFormErrorMessage(intl.formatMessage(messages.genericErrorMessage));
61
+ return [];
62
+ }
63
+ };
64
+ }
65
+ }, [intl, onFailure, loadOptionsFn, setFormErrorMessage]) : loadOptionsFn;
35
66
  useEffect(() => {
36
67
  let current = true;
37
68
  const fetch = async (query = '') => {
@@ -1,10 +1,14 @@
1
1
  /** @jsx jsx */
2
2
  import { useCallback } from 'react';
3
3
  import { css, jsx } from '@emotion/react';
4
+ import { FORM_ERROR } from 'final-form';
4
5
  import { Form, FormSpy } from 'react-final-form';
6
+ import { useIntl } from 'react-intl-next';
5
7
  import { getBooleanFF } from '@atlaskit/platform-feature-flags';
6
8
  import { Box } from '@atlaskit/primitives';
7
9
  import { CREATE_FORM_MAX_WIDTH_IN_PX, LINK_CREATE_FORM_POST_CREATE_FIELD } from '../../common/constants';
10
+ import messages from '../../common/messages';
11
+ import { useLinkCreateCallback } from '../../controllers/callback-context';
8
12
  import { useExitWarningModal } from '../../controllers/exit-warning-modal-context';
9
13
  import { useFormContext } from '../../controllers/form-context';
10
14
  import { CreateFormFooter } from './form-footer';
@@ -26,12 +30,17 @@ export const CreateForm = ({
26
30
  initialValues
27
31
  }) => {
28
32
  const {
33
+ setFormErrorMessage,
29
34
  formErrorMessage,
30
35
  enableEditView
31
36
  } = useFormContext();
37
+ const intl = useIntl();
32
38
  const {
33
39
  setShouldShowWarning
34
40
  } = useExitWarningModal();
41
+ const {
42
+ onFailure
43
+ } = useLinkCreateCallback();
35
44
  const handleSubmit = useCallback(async data => {
36
45
  if (getBooleanFF('platform.linking-platform.link-create.enable-edit')) {
37
46
  const {
@@ -45,10 +54,38 @@ export const CreateForm = ({
45
54
  * if submission is successful
46
55
  */
47
56
  enableEditView === null || enableEditView === void 0 ? void 0 : enableEditView(!!shouldEnableEditView);
57
+
58
+ /**
59
+ * This is the onSubmit handler provided by the plugin
60
+ * It will be async, and it will likely involve awaiting `onCreate` (the adopters handler)
61
+ */
48
62
  return onSubmit(formData);
49
63
  }
50
64
  return onSubmit(data);
51
65
  }, [onSubmit, enableEditView]);
66
+ const handleSubmitWithErrorHandling = useCallback(async (...args) => {
67
+ try {
68
+ /**
69
+ * Clear any error message that may have been set by async select fields
70
+ * This will immediately remove any indication of an error, but the form likely will fail to submit,
71
+ * it will be likely a 400 because the user probably could not set all fields anyway
72
+ */
73
+ setFormErrorMessage();
74
+ return await handleSubmit(...args);
75
+ } catch (error) {
76
+ /**
77
+ * Notify link create of failed experience
78
+ */
79
+ onFailure === null || onFailure === void 0 ? void 0 : onFailure(error);
80
+
81
+ /**
82
+ * Return a generic message for react final form to render
83
+ */
84
+ return {
85
+ [FORM_ERROR]: intl.formatMessage(messages.genericErrorMessage)
86
+ };
87
+ }
88
+ }, [handleSubmit, setFormErrorMessage, intl, onFailure]);
52
89
  const handleCancel = useCallback(() => {
53
90
  onCancel && onCancel();
54
91
  }, [onCancel]);
@@ -56,7 +93,7 @@ export const CreateForm = ({
56
93
  return jsx(CreateFormLoader, null);
57
94
  }
58
95
  return jsx(Form, {
59
- onSubmit: handleSubmit,
96
+ onSubmit: getBooleanFF('platform.linking-platform.link-create.better-observability') ? handleSubmitWithErrorHandling : handleSubmit,
60
97
  initialValues: initialValues,
61
98
  mutators: {
62
99
  setField: (args, state, tools) => {
@@ -65,6 +102,7 @@ export const CreateForm = ({
65
102
  }
66
103
  }, ({
67
104
  submitting,
105
+ submitError,
68
106
  ...formProps
69
107
  }) => {
70
108
  return jsx("form", {
@@ -85,8 +123,14 @@ export const CreateForm = ({
85
123
  const isModified = Object.values(state.modified).some(value => value);
86
124
  setShouldShowWarning(isModified);
87
125
  }
88
- }), jsx(Box, null, children), !hideFooter && jsx(CreateFormFooter, {
89
- formErrorMessage: formErrorMessage,
126
+ }), jsx(Box, null, children), !hideFooter && jsx(CreateFormFooter
127
+ /**
128
+ * We will prefer to render the error message connected to
129
+ * react final form state (submitError) otherwise we can
130
+ * default to the `formErrorMessage` that we sometimes use with our own
131
+ * "form context" (only currently used for AsyncSelect field reporting failed loading)
132
+ */, {
133
+ formErrorMessage: getBooleanFF('platform.linking-platform.link-create.better-observability') ? submitError || formErrorMessage : formErrorMessage,
90
134
  handleCancel: handleCancel,
91
135
  submitting: submitting,
92
136
  testId: testId