@atlaskit/link-picker 1.6.1 → 1.7.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 (42) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cjs/{ui/error-boundary-fallback → common}/generic-error-svg/index.js +0 -0
  3. package/dist/cjs/{ui/error-boundary-fallback → common}/generic-error-svg/styled.js +0 -0
  4. package/dist/cjs/services/use-plugins/index.js +32 -35
  5. package/dist/cjs/services/use-search-query/index.js +41 -0
  6. package/dist/cjs/ui/error-boundary-fallback/index.js +1 -1
  7. package/dist/cjs/ui/link-picker/index.js +56 -40
  8. package/dist/cjs/ui/link-picker/link-search-error/index.js +27 -0
  9. package/dist/cjs/ui/link-picker/messages.js +10 -0
  10. package/dist/cjs/ui/link-picker/url.js +0 -5
  11. package/dist/cjs/version.json +1 -1
  12. package/dist/es2019/{ui/error-boundary-fallback → common}/generic-error-svg/index.js +0 -0
  13. package/dist/es2019/{ui/error-boundary-fallback → common}/generic-error-svg/styled.js +0 -0
  14. package/dist/es2019/services/use-plugins/index.js +28 -38
  15. package/dist/es2019/services/use-search-query/index.js +32 -0
  16. package/dist/es2019/ui/error-boundary-fallback/index.js +1 -1
  17. package/dist/es2019/ui/link-picker/index.js +50 -39
  18. package/dist/es2019/ui/link-picker/link-search-error/index.js +10 -0
  19. package/dist/es2019/ui/link-picker/messages.js +10 -0
  20. package/dist/es2019/ui/link-picker/url.js +0 -5
  21. package/dist/es2019/version.json +1 -1
  22. package/dist/esm/{ui/error-boundary-fallback → common}/generic-error-svg/index.js +0 -0
  23. package/dist/esm/{ui/error-boundary-fallback → common}/generic-error-svg/styled.js +0 -0
  24. package/dist/esm/services/use-plugins/index.js +33 -35
  25. package/dist/esm/services/use-search-query/index.js +32 -0
  26. package/dist/esm/ui/error-boundary-fallback/index.js +1 -1
  27. package/dist/esm/ui/link-picker/index.js +52 -39
  28. package/dist/esm/ui/link-picker/link-search-error/index.js +14 -0
  29. package/dist/esm/ui/link-picker/messages.js +10 -0
  30. package/dist/esm/ui/link-picker/url.js +0 -5
  31. package/dist/esm/version.json +1 -1
  32. package/dist/types/{ui/error-boundary-fallback → common}/generic-error-svg/index.d.ts +0 -0
  33. package/dist/types/{ui/error-boundary-fallback → common}/generic-error-svg/styled.d.ts +0 -0
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/services/use-plugins/index.d.ts +6 -5
  36. package/dist/types/services/use-search-query/index.d.ts +3 -0
  37. package/dist/types/ui/link-picker/index.d.ts +2 -0
  38. package/dist/types/ui/link-picker/link-search-error/index.d.ts +4 -0
  39. package/dist/types/ui/link-picker/messages.d.ts +10 -0
  40. package/dist/types/ui/types.d.ts +5 -2
  41. package/package.json +4 -4
  42. package/report.api.md +11 -5
@@ -1,7 +1,7 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
2
 
3
3
  /** @jsx jsx */
4
- import { useCallback, useLayoutEffect, useReducer } from 'react';
4
+ import { Fragment, useCallback, useLayoutEffect, useReducer } from 'react';
5
5
  import { jsx } from '@emotion/react';
6
6
  import { useIntl, FormattedMessage } from 'react-intl-next';
7
7
  import Button, { ButtonGroup } from '@atlaskit/button';
@@ -9,17 +9,20 @@ import { FormFooter } from '@atlaskit/form';
9
9
  import EditorSearchIcon from '@atlaskit/icon/glyph/editor/search';
10
10
  import Tabs, { Tab, TabList } from '@atlaskit/tabs';
11
11
  import VisuallyHidden from '@atlaskit/visually-hidden';
12
+ import { usePlugins } from '../../services/use-plugins';
13
+ import { useSearchQuery } from '../../services/use-search-query';
12
14
  import { messages } from './messages';
13
15
  import PanelTextInput from './text-input';
14
16
  import LinkSearchList from './link-search-list';
15
17
  import { normalizeUrl, isSafeUrl } from './url';
16
18
  import { rootContainerStyles, searchIconStyles, listTitleStyles, tabsWrapperStyles } from './styled';
17
19
  import { browser } from './browser';
18
- import { transformTimeStamp } from './transformTimeStamp';
19
20
  import Announcer from './announcer';
20
21
  import LinkSearchNoResults from './link-search-no-results';
21
- import { usePlugins } from '../../services/use-plugins';
22
+ import { transformTimeStamp } from './transformTimeStamp';
23
+ import LinkSearchError from './link-search-error';
22
24
  export const RECENT_SEARCH_LIST_SIZE = 5;
25
+ export const CONTACT_SUPPORT_LINK = 'https://support.atlassian.com/contact/';
23
26
  export const testIds = {
24
27
  linkPicker: 'link-picker',
25
28
  urlInputField: 'link-url',
@@ -35,7 +38,8 @@ export const testIds = {
35
38
  searchResultLoadingIndicator: 'link-picker.results-loading-indicator',
36
39
  urlError: 'link-error',
37
40
  tabList: 'link-picker-tabs',
38
- tabItem: 'link-picker-tab'
41
+ tabItem: 'link-picker-tab',
42
+ searchError: 'link-search-error'
39
43
  };
40
44
  const initState = {
41
45
  url: '',
@@ -68,6 +72,8 @@ function LinkPicker({
68
72
  url: initUrl,
69
73
  displayText: initDisplayText
70
74
  }) {
75
+ var _queryState$query, _items$length, _errorFallback;
76
+
71
77
  const [state, dispatch] = useReducer(reducer, { ...initState,
72
78
  url: normalizeUrl(initUrl) || '',
73
79
  displayText: initDisplayText || ''
@@ -77,17 +83,23 @@ function LinkPicker({
77
83
  selectedIndex,
78
84
  url,
79
85
  displayText,
80
- invalidUrl
86
+ invalidUrl,
87
+ activeTab
81
88
  } = state;
82
- const isEditing = !!initUrl;
83
89
  const intl = useIntl();
90
+ const queryState = useSearchQuery(state);
84
91
  const {
85
92
  items,
86
93
  isLoading,
87
- isSelectedItem,
88
94
  isActivePlugin,
89
- tabs
90
- } = usePlugins(state, plugins);
95
+ tabs,
96
+ error,
97
+ retry,
98
+ errorFallback
99
+ } = usePlugins(queryState, activeTab, plugins);
100
+ const isEditing = !!initUrl;
101
+ const selectedItem = items === null || items === void 0 ? void 0 : items[selectedIndex];
102
+ const isSelectedItem = (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.url) === url;
91
103
  useLayoutEffect(() => {
92
104
  if (onContentResize) {
93
105
  onContentResize();
@@ -145,13 +157,13 @@ function LinkPicker({
145
157
  });
146
158
  }, []);
147
159
  const handleMouseEnterResultItem = useCallback(objectId => {
148
- const index = findIndex(items, item => item.objectId === objectId);
160
+ const index = items === null || items === void 0 ? void 0 : items.findIndex(item => item.objectId === objectId);
149
161
  dispatch({
150
- activeIndex: index
162
+ activeIndex: index !== null && index !== void 0 ? index : -1
151
163
  });
152
164
  }, [items]);
153
165
  const handleMouseLeaveResultItem = useCallback(objectId => {
154
- const index = findIndex(items, item => item.objectId === objectId); // This is to avoid updating index that was set by other mouseenter event
166
+ const index = items === null || items === void 0 ? void 0 : items.findIndex(item => item.objectId === objectId); // This is to avoid updating index that was set by other mouseenter event
155
167
 
156
168
  if (activeIndex === index) {
157
169
  dispatch({
@@ -173,7 +185,7 @@ function LinkPicker({
173
185
  });
174
186
  }, [displayText, onSubmit, state.url]);
175
187
  const handleSelected = useCallback(objectId => {
176
- const selectedItem = items.find(item => item.objectId === objectId);
188
+ const selectedItem = items === null || items === void 0 ? void 0 : items.find(item => item.objectId === objectId);
177
189
 
178
190
  if (selectedItem) {
179
191
  const {
@@ -184,9 +196,7 @@ function LinkPicker({
184
196
  }
185
197
  }, [handleInsert, items]);
186
198
  const handleSubmit = useCallback(() => {
187
- const selectedItem = items[selectedIndex];
188
-
189
- if (isSelectedItem) {
199
+ if (isSelectedItem && selectedItem) {
190
200
  return handleInsert(selectedItem.url, selectedItem.name, 'typeAhead');
191
201
  }
192
202
 
@@ -199,18 +209,18 @@ function LinkPicker({
199
209
  return dispatch({
200
210
  invalidUrl: true
201
211
  });
202
- }, [handleInsert, isSelectedItem, items, selectedIndex, url]);
212
+ }, [handleInsert, isSelectedItem, selectedItem, url]);
203
213
  const linkPlaceHolder = isActivePlugin ? messages.placeholder : messages.linkPlaceholder;
204
- const linkListTitle = url ? messages.titleResults : messages.titleRecentlyViewed;
214
+ const linkListTitle = (queryState === null || queryState === void 0 ? void 0 : (_queryState$query = queryState.query) === null || _queryState$query === void 0 ? void 0 : _queryState$query.length) !== 0 ? messages.titleResults : messages.titleRecentlyViewed;
205
215
  const insertButtonMsg = isEditing ? messages.saveButton : messages.insertButton;
206
- const noResults = isActivePlugin && !isLoading && !items.length && !isSafeUrl(url);
216
+ const noResults = (items === null || items === void 0 ? void 0 : items.length) === 0 && !error;
207
217
  const screenReaderDescriptionId = 'search-recent-links-field-description';
208
218
  const linkSearchListId = 'link-picker-search-list';
209
219
  const ariaActiveDescendant = selectedIndex > -1 ? `link-search-list-item-${selectedIndex}` : ''; // Added workaround with a screen reader Announcer specifically for VoiceOver + Safari
210
220
  // as the Aria design pattern for combobox does not work in this case
211
221
  // for details: https://a11y-internal.atlassian.net/browse/AK-740
212
222
 
213
- const screenReaderText = browser.safari && getScreenReaderText(items, selectedIndex, intl);
223
+ const screenReaderText = browser.safari && getScreenReaderText(items !== null && items !== void 0 ? items : [], selectedIndex, intl);
214
224
  const searchIcon = isActivePlugin && jsx("span", {
215
225
  css: searchIconStyles,
216
226
  "data-testid": testIds.searchIcon
@@ -262,7 +272,7 @@ function LinkPicker({
262
272
  onSubmit: handleSubmit,
263
273
  onKeyDown: handleKeyDown,
264
274
  onChange: handleChangeText
265
- }), tabs.length > 0 && jsx("div", {
275
+ }), queryState && jsx(Fragment, null, tabs.length > 0 && jsx("div", {
266
276
  css: tabsWrapperStyles
267
277
  }, jsx(Tabs, {
268
278
  id: testIds.tabList,
@@ -276,7 +286,7 @@ function LinkPicker({
276
286
  }, jsx(TabList, null, tabs.map(tab => jsx(Tab, {
277
287
  key: tab.tabTitle,
278
288
  testId: testIds.tabItem
279
- }, tab.tabTitle))))), !!items.length && jsx("div", {
289
+ }, tab.tabTitle))))), !!(items !== null && items !== void 0 && items.length) && jsx("div", {
280
290
  css: listTitleStyles,
281
291
  id: testIds.resultListTitle,
282
292
  "data-testid": testIds.resultListTitle
@@ -284,7 +294,7 @@ function LinkPicker({
284
294
  id: "fabric.smartcard.linkpicker.suggested.results"
285
295
  }, url && jsx(FormattedMessage, _extends({}, messages.searchLinkResults, {
286
296
  values: {
287
- count: items.length
297
+ count: (_items$length = items === null || items === void 0 ? void 0 : items.length) !== null && _items$length !== void 0 ? _items$length : 0
288
298
  },
289
299
  "aria-live": "polite",
290
300
  "aria-atomic": "true"
@@ -293,18 +303,32 @@ function LinkPicker({
293
303
  ariaControls: "fabric.smartcard.linkpicker.suggested.results",
294
304
  id: linkSearchListId,
295
305
  role: "listbox",
296
- items: items,
306
+ items: items !== null && items !== void 0 ? items : [],
297
307
  isLoading: isLoading,
298
308
  selectedIndex: selectedIndex,
299
309
  activeIndex: activeIndex,
300
310
  onSelect: handleSelected,
301
311
  onMouseEnter: handleMouseEnterResultItem,
302
312
  onMouseLeave: handleMouseLeaveResultItem
303
- }), noResults && jsx(LinkSearchNoResults, {
313
+ }), error && ((_errorFallback = errorFallback === null || errorFallback === void 0 ? void 0 : errorFallback(error, retry)) !== null && _errorFallback !== void 0 ? _errorFallback : jsx(LinkSearchError, {
314
+ testId: testIds.searchError,
315
+ header: intl.formatMessage(messages.searchErrorHeader),
316
+ description: jsx(FormattedMessage, _extends({}, messages.searchErrorDescription, {
317
+ values: {
318
+ a: label => jsx(Button, {
319
+ appearance: "link",
320
+ spacing: "none",
321
+ href: CONTACT_SUPPORT_LINK,
322
+ target: "_blank",
323
+ rel: "noopener noreferrer"
324
+ }, label)
325
+ }
326
+ }))
327
+ })), noResults && jsx(LinkSearchNoResults, {
304
328
  testId: testIds.emptyResultPage,
305
329
  header: intl.formatMessage(messages.noResults),
306
330
  description: intl.formatMessage(messages.noResultsDescription)
307
- }), jsx(FormFooter, {
331
+ })), jsx(FormFooter, {
308
332
  align: "end"
309
333
  }, jsx(ButtonGroup, null, jsx(Button, {
310
334
  appearance: "default",
@@ -332,17 +356,4 @@ function getScreenReaderText(items, selectedIndex, intl) {
332
356
  const formattedDate = [date === null || date === void 0 ? void 0 : date.pageAction, date === null || date === void 0 ? void 0 : date.dateString, date === null || date === void 0 ? void 0 : date.timeSince].filter(Boolean).join(' ');
333
357
  return [name, container, formattedDate].filter(Boolean).join(', ');
334
358
  }
335
- }
336
-
337
- function findIndex(array, predicate) {
338
- let index = -1;
339
- array.some((item, i) => {
340
- if (predicate(item)) {
341
- index = i;
342
- return true;
343
- }
344
-
345
- return false;
346
- });
347
- return index;
348
359
  }
@@ -0,0 +1,10 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import React from 'react';
3
+ import EmptyState from '@atlaskit/empty-state';
4
+ import GenericErrorSVG from '../../../common/generic-error-svg';
5
+
6
+ const LinkSearchError = props => /*#__PURE__*/React.createElement(EmptyState, _extends({}, props, {
7
+ renderImage: () => /*#__PURE__*/React.createElement(GenericErrorSVG, null)
8
+ }));
9
+
10
+ export default LinkSearchError;
@@ -104,5 +104,15 @@ export const messages = defineMessages({
104
104
  id: 'fabric.linkPicker.button.save',
105
105
  defaultMessage: 'Save',
106
106
  description: 'Button to save edited link'
107
+ },
108
+ searchErrorHeader: {
109
+ id: 'fabric.linkPicker.search.error.heading',
110
+ defaultMessage: 'We’re having trouble loading data.',
111
+ description: 'Heading message shown when a search throws an error'
112
+ },
113
+ searchErrorDescription: {
114
+ id: 'fabric.linkPicker.search.error.description',
115
+ defaultMessage: 'Refresh the page, or contact <a>Atlassian Support</a> if this keeps happening.',
116
+ description: 'Describes possible actions when search throws an error'
107
117
  }
108
118
  });
@@ -64,11 +64,6 @@ const linkifyMatch = text => {
64
64
 
65
65
  export const isSafeUrl = url => {
66
66
  const urlTrimmed = url.trim();
67
-
68
- if (urlTrimmed.length === 0) {
69
- return true;
70
- }
71
-
72
67
  return whitelistedURLPatterns.some(p => p.test(urlTrimmed));
73
68
  };
74
69
  /**
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/link-picker",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "sideEffects": false
5
5
  }
@@ -7,64 +7,53 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (O
7
7
 
8
8
  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) { _defineProperty(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; }
9
9
 
10
- import { useEffect, useRef, useReducer, useMemo } from 'react';
10
+ import { useEffect, useReducer, useMemo, useRef, useState, useCallback } from 'react';
11
11
  import { RECENT_SEARCH_LIST_SIZE } from '../../ui/link-picker';
12
- import { isSafeUrl } from '../../ui/link-picker/url';
13
12
 
14
13
  function reducer(state, payload) {
15
14
  return _objectSpread(_objectSpread({}, state), payload);
16
15
  }
17
16
 
18
- export function usePlugins(state, plugins) {
17
+ export function usePlugins(state, activeTab, plugins) {
18
+ var _useState = useState(0),
19
+ _useState2 = _slicedToArray(_useState, 2),
20
+ retries = _useState2[0],
21
+ setRetries = _useState2[1];
22
+
19
23
  var _useReducer = useReducer(reducer, {
20
- items: [],
24
+ items: null,
25
+ error: null,
21
26
  isLoading: false
22
27
  }),
23
28
  _useReducer2 = _slicedToArray(_useReducer, 2),
24
29
  pluginState = _useReducer2[0],
25
30
  dispatch = _useReducer2[1];
26
31
 
27
- var url = state.url,
28
- selectedIndex = state.selectedIndex;
29
32
  var items = pluginState.items,
30
- isLoading = pluginState.isLoading;
31
- var activePlugin = !plugins || plugins.length === 0 ? null : plugins[state.activeTab];
33
+ isLoading = pluginState.isLoading,
34
+ error = pluginState.error;
32
35
  var queryVersion = useRef(0);
33
- var pluginRef = useRef(activePlugin);
34
- var selectedItem = items[selectedIndex];
35
- var isSelectedItem = (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.url) === url;
36
+ var activePlugin = plugins === null || plugins === void 0 ? void 0 : plugins[activeTab];
36
37
  useEffect(function () {
37
38
  if (!activePlugin) {
38
39
  return;
39
40
  }
40
41
 
41
- if (isSelectedItem) {
42
- return;
43
- }
44
-
45
- if (isSafeUrl(url) && url.length) {
42
+ if (!state) {
46
43
  dispatch({
47
- items: [],
48
- isLoading: false
44
+ items: null,
45
+ isLoading: false,
46
+ error: null
49
47
  });
50
48
  return;
51
49
  }
52
50
 
53
- var updates = activePlugin.resolve({
54
- query: url
51
+ var updates = activePlugin.resolve(state);
52
+ dispatch({
53
+ items: null,
54
+ isLoading: true,
55
+ error: null
55
56
  });
56
-
57
- if (pluginRef.current !== activePlugin) {
58
- dispatch({
59
- items: [],
60
- isLoading: true
61
- });
62
- } else {
63
- dispatch({
64
- isLoading: true
65
- });
66
- }
67
-
68
57
  var newQuery = ++queryVersion.current;
69
58
 
70
59
  var updateResults = /*#__PURE__*/function () {
@@ -142,6 +131,8 @@ export function usePlugins(state, plugins) {
142
131
  _context.prev = 25;
143
132
  _context.t0 = _context["catch"](0);
144
133
  dispatch({
134
+ error: _context.t0,
135
+ items: null,
145
136
  isLoading: false
146
137
  });
147
138
 
@@ -159,7 +150,7 @@ export function usePlugins(state, plugins) {
159
150
  }();
160
151
 
161
152
  updateResults();
162
- }, [activePlugin, queryVersion, url, isSelectedItem]);
153
+ }, [activePlugin, queryVersion, state, retries]);
163
154
  var tabs = useMemo(function () {
164
155
  if (!plugins || plugins.length <= 1) {
165
156
  return [];
@@ -173,12 +164,19 @@ export function usePlugins(state, plugins) {
173
164
  };
174
165
  });
175
166
  }, [plugins]);
167
+ var handleRetry = useCallback(function () {
168
+ setRetries(function (prev) {
169
+ return ++prev;
170
+ });
171
+ }, []);
176
172
  return {
177
173
  tabs: tabs,
178
174
  items: items,
179
175
  isLoading: isLoading,
180
- isSelectedItem: isSelectedItem,
181
- isActivePlugin: !!activePlugin
176
+ isActivePlugin: !!activePlugin,
177
+ error: error,
178
+ retry: handleRetry,
179
+ errorFallback: activePlugin === null || activePlugin === void 0 ? void 0 : activePlugin.errorFallback
182
180
  };
183
181
  }
184
182
 
@@ -0,0 +1,32 @@
1
+ import { useRef } from 'react';
2
+ import { isSafeUrl } from '../../ui/link-picker/url';
3
+ export function useSearchQuery(state) {
4
+ var _queryState$current;
5
+
6
+ var queryState = useRef(null);
7
+ /*
8
+ * When state contains a valid url AND is NOT selected from results
9
+ * queryState should be null to clear the items in plugin state
10
+ */
11
+
12
+ if (isSafeUrl(state.url) && state.selectedIndex === -1) {
13
+ queryState.current = null;
14
+ }
15
+ /*
16
+ * When state contains a search term e.g. Not a valid url return a query
17
+ */
18
+
19
+
20
+ if (((_queryState$current = queryState.current) === null || _queryState$current === void 0 ? void 0 : _queryState$current.query) !== state.url && !isSafeUrl(state.url)) {
21
+ queryState.current = {
22
+ query: state.url
23
+ };
24
+ }
25
+ /*
26
+ * When state contains a valid url and is selected from the list
27
+ * return current state to prevent triger a new plugin search
28
+ */
29
+
30
+
31
+ return queryState.current;
32
+ }
@@ -2,7 +2,7 @@
2
2
  import { jsx } from '@emotion/react';
3
3
  import EmptyState from '@atlaskit/empty-state';
4
4
  import { defineMessages, useIntl } from 'react-intl-next';
5
- import GenericErrorSVG from './generic-error-svg';
5
+ import GenericErrorSVG from '../../common/generic-error-svg';
6
6
  import { rootContainerStyles } from '../link-picker/styled';
7
7
  var messages = defineMessages({
8
8
  heading: {
@@ -7,7 +7,7 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (O
7
7
  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) { _defineProperty(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; }
8
8
 
9
9
  /** @jsx jsx */
10
- import { useCallback, useLayoutEffect, useReducer } from 'react';
10
+ import { Fragment, useCallback, useLayoutEffect, useReducer } from 'react';
11
11
  import { jsx } from '@emotion/react';
12
12
  import { useIntl, FormattedMessage } from 'react-intl-next';
13
13
  import Button, { ButtonGroup } from '@atlaskit/button';
@@ -15,17 +15,20 @@ import { FormFooter } from '@atlaskit/form';
15
15
  import EditorSearchIcon from '@atlaskit/icon/glyph/editor/search';
16
16
  import Tabs, { Tab, TabList } from '@atlaskit/tabs';
17
17
  import VisuallyHidden from '@atlaskit/visually-hidden';
18
+ import { usePlugins } from '../../services/use-plugins';
19
+ import { useSearchQuery } from '../../services/use-search-query';
18
20
  import { messages } from './messages';
19
21
  import PanelTextInput from './text-input';
20
22
  import LinkSearchList from './link-search-list';
21
23
  import { normalizeUrl, isSafeUrl } from './url';
22
24
  import { rootContainerStyles, searchIconStyles, listTitleStyles, tabsWrapperStyles } from './styled';
23
25
  import { browser } from './browser';
24
- import { transformTimeStamp } from './transformTimeStamp';
25
26
  import Announcer from './announcer';
26
27
  import LinkSearchNoResults from './link-search-no-results';
27
- import { usePlugins } from '../../services/use-plugins';
28
+ import { transformTimeStamp } from './transformTimeStamp';
29
+ import LinkSearchError from './link-search-error';
28
30
  export var RECENT_SEARCH_LIST_SIZE = 5;
31
+ export var CONTACT_SUPPORT_LINK = 'https://support.atlassian.com/contact/';
29
32
  export var testIds = {
30
33
  linkPicker: 'link-picker',
31
34
  urlInputField: 'link-url',
@@ -41,7 +44,8 @@ export var testIds = {
41
44
  searchResultLoadingIndicator: 'link-picker.results-loading-indicator',
42
45
  urlError: 'link-error',
43
46
  tabList: 'link-picker-tabs',
44
- tabItem: 'link-picker-tab'
47
+ tabItem: 'link-picker-tab',
48
+ searchError: 'link-search-error'
45
49
  };
46
50
  var initState = {
47
51
  url: '',
@@ -64,6 +68,8 @@ function reducer(state, payload) {
64
68
  }
65
69
 
66
70
  function LinkPicker(_ref) {
71
+ var _queryState$query, _items$length, _errorFallback;
72
+
67
73
  var onSubmit = _ref.onSubmit,
68
74
  onCancel = _ref.onCancel,
69
75
  onContentResize = _ref.onContentResize,
@@ -83,17 +89,23 @@ function LinkPicker(_ref) {
83
89
  selectedIndex = state.selectedIndex,
84
90
  url = state.url,
85
91
  displayText = state.displayText,
86
- invalidUrl = state.invalidUrl;
87
- var isEditing = !!initUrl;
92
+ invalidUrl = state.invalidUrl,
93
+ activeTab = state.activeTab;
88
94
  var intl = useIntl();
95
+ var queryState = useSearchQuery(state);
89
96
 
90
- var _usePlugins = usePlugins(state, plugins),
97
+ var _usePlugins = usePlugins(queryState, activeTab, plugins),
91
98
  items = _usePlugins.items,
92
99
  isLoading = _usePlugins.isLoading,
93
- isSelectedItem = _usePlugins.isSelectedItem,
94
100
  isActivePlugin = _usePlugins.isActivePlugin,
95
- tabs = _usePlugins.tabs;
101
+ tabs = _usePlugins.tabs,
102
+ error = _usePlugins.error,
103
+ retry = _usePlugins.retry,
104
+ errorFallback = _usePlugins.errorFallback;
96
105
 
106
+ var isEditing = !!initUrl;
107
+ var selectedItem = items === null || items === void 0 ? void 0 : items[selectedIndex];
108
+ var isSelectedItem = (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.url) === url;
97
109
  useLayoutEffect(function () {
98
110
  if (onContentResize) {
99
111
  onContentResize();
@@ -147,15 +159,15 @@ function LinkPicker(_ref) {
147
159
  dispatch(_defineProperty({}, field, ''));
148
160
  }, []);
149
161
  var handleMouseEnterResultItem = useCallback(function (objectId) {
150
- var index = findIndex(items, function (item) {
162
+ var index = items === null || items === void 0 ? void 0 : items.findIndex(function (item) {
151
163
  return item.objectId === objectId;
152
164
  });
153
165
  dispatch({
154
- activeIndex: index
166
+ activeIndex: index !== null && index !== void 0 ? index : -1
155
167
  });
156
168
  }, [items]);
157
169
  var handleMouseLeaveResultItem = useCallback(function (objectId) {
158
- var index = findIndex(items, function (item) {
170
+ var index = items === null || items === void 0 ? void 0 : items.findIndex(function (item) {
159
171
  return item.objectId === objectId;
160
172
  }); // This is to avoid updating index that was set by other mouseenter event
161
173
 
@@ -178,7 +190,7 @@ function LinkPicker(_ref) {
178
190
  } : {}));
179
191
  }, [displayText, onSubmit, state.url]);
180
192
  var handleSelected = useCallback(function (objectId) {
181
- var selectedItem = items.find(function (item) {
193
+ var selectedItem = items === null || items === void 0 ? void 0 : items.find(function (item) {
182
194
  return item.objectId === objectId;
183
195
  });
184
196
 
@@ -189,9 +201,7 @@ function LinkPicker(_ref) {
189
201
  }
190
202
  }, [handleInsert, items]);
191
203
  var handleSubmit = useCallback(function () {
192
- var selectedItem = items[selectedIndex];
193
-
194
- if (isSelectedItem) {
204
+ if (isSelectedItem && selectedItem) {
195
205
  return handleInsert(selectedItem.url, selectedItem.name, 'typeAhead');
196
206
  }
197
207
 
@@ -204,18 +214,18 @@ function LinkPicker(_ref) {
204
214
  return dispatch({
205
215
  invalidUrl: true
206
216
  });
207
- }, [handleInsert, isSelectedItem, items, selectedIndex, url]);
217
+ }, [handleInsert, isSelectedItem, selectedItem, url]);
208
218
  var linkPlaceHolder = isActivePlugin ? messages.placeholder : messages.linkPlaceholder;
209
- var linkListTitle = url ? messages.titleResults : messages.titleRecentlyViewed;
219
+ var linkListTitle = (queryState === null || queryState === void 0 ? void 0 : (_queryState$query = queryState.query) === null || _queryState$query === void 0 ? void 0 : _queryState$query.length) !== 0 ? messages.titleResults : messages.titleRecentlyViewed;
210
220
  var insertButtonMsg = isEditing ? messages.saveButton : messages.insertButton;
211
- var noResults = isActivePlugin && !isLoading && !items.length && !isSafeUrl(url);
221
+ var noResults = (items === null || items === void 0 ? void 0 : items.length) === 0 && !error;
212
222
  var screenReaderDescriptionId = 'search-recent-links-field-description';
213
223
  var linkSearchListId = 'link-picker-search-list';
214
224
  var ariaActiveDescendant = selectedIndex > -1 ? "link-search-list-item-".concat(selectedIndex) : ''; // Added workaround with a screen reader Announcer specifically for VoiceOver + Safari
215
225
  // as the Aria design pattern for combobox does not work in this case
216
226
  // for details: https://a11y-internal.atlassian.net/browse/AK-740
217
227
 
218
- var screenReaderText = browser.safari && getScreenReaderText(items, selectedIndex, intl);
228
+ var screenReaderText = browser.safari && getScreenReaderText(items !== null && items !== void 0 ? items : [], selectedIndex, intl);
219
229
  var searchIcon = isActivePlugin && jsx("span", {
220
230
  css: searchIconStyles,
221
231
  "data-testid": testIds.searchIcon
@@ -267,7 +277,7 @@ function LinkPicker(_ref) {
267
277
  onSubmit: handleSubmit,
268
278
  onKeyDown: handleKeyDown,
269
279
  onChange: handleChangeText
270
- }), tabs.length > 0 && jsx("div", {
280
+ }), queryState && jsx(Fragment, null, tabs.length > 0 && jsx("div", {
271
281
  css: tabsWrapperStyles
272
282
  }, jsx(Tabs, {
273
283
  id: testIds.tabList,
@@ -285,7 +295,7 @@ function LinkPicker(_ref) {
285
295
  key: tab.tabTitle,
286
296
  testId: testIds.tabItem
287
297
  }, tab.tabTitle);
288
- })))), !!items.length && jsx("div", {
298
+ })))), !!(items !== null && items !== void 0 && items.length) && jsx("div", {
289
299
  css: listTitleStyles,
290
300
  id: testIds.resultListTitle,
291
301
  "data-testid": testIds.resultListTitle
@@ -293,7 +303,7 @@ function LinkPicker(_ref) {
293
303
  id: "fabric.smartcard.linkpicker.suggested.results"
294
304
  }, url && jsx(FormattedMessage, _extends({}, messages.searchLinkResults, {
295
305
  values: {
296
- count: items.length
306
+ count: (_items$length = items === null || items === void 0 ? void 0 : items.length) !== null && _items$length !== void 0 ? _items$length : 0
297
307
  },
298
308
  "aria-live": "polite",
299
309
  "aria-atomic": "true"
@@ -302,18 +312,34 @@ function LinkPicker(_ref) {
302
312
  ariaControls: "fabric.smartcard.linkpicker.suggested.results",
303
313
  id: linkSearchListId,
304
314
  role: "listbox",
305
- items: items,
315
+ items: items !== null && items !== void 0 ? items : [],
306
316
  isLoading: isLoading,
307
317
  selectedIndex: selectedIndex,
308
318
  activeIndex: activeIndex,
309
319
  onSelect: handleSelected,
310
320
  onMouseEnter: handleMouseEnterResultItem,
311
321
  onMouseLeave: handleMouseLeaveResultItem
312
- }), noResults && jsx(LinkSearchNoResults, {
322
+ }), error && ((_errorFallback = errorFallback === null || errorFallback === void 0 ? void 0 : errorFallback(error, retry)) !== null && _errorFallback !== void 0 ? _errorFallback : jsx(LinkSearchError, {
323
+ testId: testIds.searchError,
324
+ header: intl.formatMessage(messages.searchErrorHeader),
325
+ description: jsx(FormattedMessage, _extends({}, messages.searchErrorDescription, {
326
+ values: {
327
+ a: function a(label) {
328
+ return jsx(Button, {
329
+ appearance: "link",
330
+ spacing: "none",
331
+ href: CONTACT_SUPPORT_LINK,
332
+ target: "_blank",
333
+ rel: "noopener noreferrer"
334
+ }, label);
335
+ }
336
+ }
337
+ }))
338
+ })), noResults && jsx(LinkSearchNoResults, {
313
339
  testId: testIds.emptyResultPage,
314
340
  header: intl.formatMessage(messages.noResults),
315
341
  description: intl.formatMessage(messages.noResultsDescription)
316
- }), jsx(FormFooter, {
342
+ })), jsx(FormFooter, {
317
343
  align: "end"
318
344
  }, jsx(ButtonGroup, null, jsx(Button, {
319
345
  appearance: "default",
@@ -340,17 +366,4 @@ function getScreenReaderText(items, selectedIndex, intl) {
340
366
  var formattedDate = [date === null || date === void 0 ? void 0 : date.pageAction, date === null || date === void 0 ? void 0 : date.dateString, date === null || date === void 0 ? void 0 : date.timeSince].filter(Boolean).join(' ');
341
367
  return [name, container, formattedDate].filter(Boolean).join(', ');
342
368
  }
343
- }
344
-
345
- function findIndex(array, predicate) {
346
- var index = -1;
347
- array.some(function (item, i) {
348
- if (predicate(item)) {
349
- index = i;
350
- return true;
351
- }
352
-
353
- return false;
354
- });
355
- return index;
356
369
  }