@atlaskit/link-picker 1.5.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 +18 -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 +62 -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 +56 -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 +58 -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 +5 -1
  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 +6 -5
  42. package/report.api.md +21 -13
@@ -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
  const messages = defineMessages({
8
8
  heading: {
@@ -1,7 +1,7 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
2
 
3
3
  /** @jsx jsx */
4
- import { useCallback, 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: '',
@@ -63,10 +67,13 @@ function reducer(state, payload) {
63
67
  function LinkPicker({
64
68
  onSubmit,
65
69
  onCancel,
70
+ onContentResize,
66
71
  plugins,
67
72
  url: initUrl,
68
73
  displayText: initDisplayText
69
74
  }) {
75
+ var _queryState$query, _items$length, _errorFallback;
76
+
70
77
  const [state, dispatch] = useReducer(reducer, { ...initState,
71
78
  url: normalizeUrl(initUrl) || '',
72
79
  displayText: initDisplayText || ''
@@ -76,17 +83,28 @@ function LinkPicker({
76
83
  selectedIndex,
77
84
  url,
78
85
  displayText,
79
- invalidUrl
86
+ invalidUrl,
87
+ activeTab
80
88
  } = state;
81
- const isEditing = !!initUrl;
82
89
  const intl = useIntl();
90
+ const queryState = useSearchQuery(state);
83
91
  const {
84
92
  items,
85
93
  isLoading,
86
- isSelectedItem,
87
94
  isActivePlugin,
88
- tabs
89
- } = 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;
103
+ useLayoutEffect(() => {
104
+ if (onContentResize) {
105
+ onContentResize();
106
+ }
107
+ }, [onContentResize, items, isLoading, isActivePlugin, tabs]);
90
108
  const handleChangeUrl = useCallback(url => {
91
109
  dispatch({
92
110
  url
@@ -139,13 +157,13 @@ function LinkPicker({
139
157
  });
140
158
  }, []);
141
159
  const handleMouseEnterResultItem = useCallback(objectId => {
142
- const index = findIndex(items, item => item.objectId === objectId);
160
+ const index = items === null || items === void 0 ? void 0 : items.findIndex(item => item.objectId === objectId);
143
161
  dispatch({
144
- activeIndex: index
162
+ activeIndex: index !== null && index !== void 0 ? index : -1
145
163
  });
146
164
  }, [items]);
147
165
  const handleMouseLeaveResultItem = useCallback(objectId => {
148
- 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
149
167
 
150
168
  if (activeIndex === index) {
151
169
  dispatch({
@@ -167,7 +185,7 @@ function LinkPicker({
167
185
  });
168
186
  }, [displayText, onSubmit, state.url]);
169
187
  const handleSelected = useCallback(objectId => {
170
- const selectedItem = items.find(item => item.objectId === objectId);
188
+ const selectedItem = items === null || items === void 0 ? void 0 : items.find(item => item.objectId === objectId);
171
189
 
172
190
  if (selectedItem) {
173
191
  const {
@@ -178,9 +196,7 @@ function LinkPicker({
178
196
  }
179
197
  }, [handleInsert, items]);
180
198
  const handleSubmit = useCallback(() => {
181
- const selectedItem = items[selectedIndex];
182
-
183
- if (isSelectedItem) {
199
+ if (isSelectedItem && selectedItem) {
184
200
  return handleInsert(selectedItem.url, selectedItem.name, 'typeAhead');
185
201
  }
186
202
 
@@ -193,18 +209,18 @@ function LinkPicker({
193
209
  return dispatch({
194
210
  invalidUrl: true
195
211
  });
196
- }, [handleInsert, isSelectedItem, items, selectedIndex, url]);
212
+ }, [handleInsert, isSelectedItem, selectedItem, url]);
197
213
  const linkPlaceHolder = isActivePlugin ? messages.placeholder : messages.linkPlaceholder;
198
- 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;
199
215
  const insertButtonMsg = isEditing ? messages.saveButton : messages.insertButton;
200
- const noResults = isActivePlugin && !isLoading && !items.length && !isSafeUrl(url);
216
+ const noResults = (items === null || items === void 0 ? void 0 : items.length) === 0 && !error;
201
217
  const screenReaderDescriptionId = 'search-recent-links-field-description';
202
218
  const linkSearchListId = 'link-picker-search-list';
203
219
  const ariaActiveDescendant = selectedIndex > -1 ? `link-search-list-item-${selectedIndex}` : ''; // Added workaround with a screen reader Announcer specifically for VoiceOver + Safari
204
220
  // as the Aria design pattern for combobox does not work in this case
205
221
  // for details: https://a11y-internal.atlassian.net/browse/AK-740
206
222
 
207
- const screenReaderText = browser.safari && getScreenReaderText(items, selectedIndex, intl);
223
+ const screenReaderText = browser.safari && getScreenReaderText(items !== null && items !== void 0 ? items : [], selectedIndex, intl);
208
224
  const searchIcon = isActivePlugin && jsx("span", {
209
225
  css: searchIconStyles,
210
226
  "data-testid": testIds.searchIcon
@@ -256,7 +272,7 @@ function LinkPicker({
256
272
  onSubmit: handleSubmit,
257
273
  onKeyDown: handleKeyDown,
258
274
  onChange: handleChangeText
259
- }), tabs.length > 0 && jsx("div", {
275
+ }), queryState && jsx(Fragment, null, tabs.length > 0 && jsx("div", {
260
276
  css: tabsWrapperStyles
261
277
  }, jsx(Tabs, {
262
278
  id: testIds.tabList,
@@ -270,7 +286,7 @@ function LinkPicker({
270
286
  }, jsx(TabList, null, tabs.map(tab => jsx(Tab, {
271
287
  key: tab.tabTitle,
272
288
  testId: testIds.tabItem
273
- }, tab.tabTitle))))), !!items.length && jsx("div", {
289
+ }, tab.tabTitle))))), !!(items !== null && items !== void 0 && items.length) && jsx("div", {
274
290
  css: listTitleStyles,
275
291
  id: testIds.resultListTitle,
276
292
  "data-testid": testIds.resultListTitle
@@ -278,7 +294,7 @@ function LinkPicker({
278
294
  id: "fabric.smartcard.linkpicker.suggested.results"
279
295
  }, url && jsx(FormattedMessage, _extends({}, messages.searchLinkResults, {
280
296
  values: {
281
- count: items.length
297
+ count: (_items$length = items === null || items === void 0 ? void 0 : items.length) !== null && _items$length !== void 0 ? _items$length : 0
282
298
  },
283
299
  "aria-live": "polite",
284
300
  "aria-atomic": "true"
@@ -287,18 +303,32 @@ function LinkPicker({
287
303
  ariaControls: "fabric.smartcard.linkpicker.suggested.results",
288
304
  id: linkSearchListId,
289
305
  role: "listbox",
290
- items: items,
306
+ items: items !== null && items !== void 0 ? items : [],
291
307
  isLoading: isLoading,
292
308
  selectedIndex: selectedIndex,
293
309
  activeIndex: activeIndex,
294
310
  onSelect: handleSelected,
295
311
  onMouseEnter: handleMouseEnterResultItem,
296
312
  onMouseLeave: handleMouseLeaveResultItem
297
- }), 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, {
298
328
  testId: testIds.emptyResultPage,
299
329
  header: intl.formatMessage(messages.noResults),
300
330
  description: intl.formatMessage(messages.noResultsDescription)
301
- }), jsx(FormFooter, {
331
+ })), jsx(FormFooter, {
302
332
  align: "end"
303
333
  }, jsx(ButtonGroup, null, jsx(Button, {
304
334
  appearance: "default",
@@ -326,17 +356,4 @@ function getScreenReaderText(items, selectedIndex, intl) {
326
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(' ');
327
357
  return [name, container, formattedDate].filter(Boolean).join(', ');
328
358
  }
329
- }
330
-
331
- function findIndex(array, predicate) {
332
- let index = -1;
333
- array.some((item, i) => {
334
- if (predicate(item)) {
335
- index = i;
336
- return true;
337
- }
338
-
339
- return false;
340
- });
341
- return index;
342
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.5.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: {