@atlaskit/editor-plugin-media-insert 6.1.3 → 6.2.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.
@@ -0,0 +1,330 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import React from 'react';
3
+ import { useIntl } from 'react-intl-next';
4
+ import { isSafeUrl } from '@atlaskit/adf-schema';
5
+ import ButtonGroup from '@atlaskit/button/button-group';
6
+ import Button from '@atlaskit/button/new';
7
+ import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
8
+ import { mediaInsertMessages } from '@atlaskit/editor-common/messages';
9
+ import Form, { ErrorMessage, Field, FormFooter, MessageWrapper } from '@atlaskit/form';
10
+ import ExpandIcon from '@atlaskit/icon/core/grow-diagonal';
11
+ import { default as EditorFilePreviewIconLegacy } from '@atlaskit/icon/glyph/editor/file-preview';
12
+ import { getMediaClient } from '@atlaskit/media-client-react';
13
+ import { Box, Flex, Inline, Stack, xcss } from '@atlaskit/primitives';
14
+ import SectionMessage from '@atlaskit/section-message';
15
+ import TextField from '@atlaskit/textfield';
16
+ import { MediaCard } from './MediaCard';
17
+ import { useAnalyticsEvents } from './useAnalyticsEvents';
18
+ const PreviewBoxStyles = xcss({
19
+ borderWidth: 'border.width',
20
+ borderStyle: 'dashed',
21
+ borderColor: 'color.border',
22
+ borderRadius: 'border.radius',
23
+ height: '200px'
24
+ });
25
+ const PreviewImageStyles = xcss({
26
+ height: '200px'
27
+ });
28
+ const FormStyles = xcss({
29
+ flexGrow: 1
30
+ });
31
+ const INITIAL_PREVIEW_STATE = Object.freeze({
32
+ isLoading: false,
33
+ error: null,
34
+ warning: null,
35
+ previewInfo: null
36
+ });
37
+ const MAX_URL_LENGTH = 2048;
38
+ export const isValidUrl = value => {
39
+ try {
40
+ // Check for spaces and length first to avoid the expensive URL parsing
41
+ // Ignored via go/ees005
42
+ // eslint-disable-next-line require-unicode-regexp
43
+ if (/\s/.test(value) || value.length > MAX_URL_LENGTH) {
44
+ return false;
45
+ }
46
+ new URL(value);
47
+ } catch (e) {
48
+ return false;
49
+ }
50
+ return isSafeUrl(value);
51
+ };
52
+ const previewStateReducer = (state, action) => {
53
+ switch (action.type) {
54
+ case 'loading':
55
+ return {
56
+ ...INITIAL_PREVIEW_STATE,
57
+ isLoading: true
58
+ };
59
+ case 'error':
60
+ return {
61
+ ...INITIAL_PREVIEW_STATE,
62
+ error: action.error
63
+ };
64
+ case 'warning':
65
+ return {
66
+ ...INITIAL_PREVIEW_STATE,
67
+ warning: action.warning
68
+ };
69
+ case 'success':
70
+ return {
71
+ ...INITIAL_PREVIEW_STATE,
72
+ previewInfo: action.payload
73
+ };
74
+ case 'reset':
75
+ return INITIAL_PREVIEW_STATE;
76
+ default:
77
+ return state;
78
+ }
79
+ };
80
+ export function MediaFromURLWithForm({
81
+ mediaProvider,
82
+ dispatchAnalyticsEvent,
83
+ closeMediaInsertPicker,
84
+ insertMediaSingle,
85
+ insertExternalMediaSingle
86
+ }) {
87
+ const intl = useIntl();
88
+ const strings = {
89
+ loadPreview: intl.formatMessage(mediaInsertMessages.loadPreview),
90
+ insert: intl.formatMessage(mediaInsertMessages.insert),
91
+ pasteLinkToUpload: intl.formatMessage(mediaInsertMessages.pasteLinkToUpload),
92
+ cancel: intl.formatMessage(mediaInsertMessages.cancel),
93
+ errorMessage: intl.formatMessage(mediaInsertMessages.fromUrlErrorMessage),
94
+ warning: intl.formatMessage(mediaInsertMessages.fromUrlWarning),
95
+ invalidUrl: intl.formatMessage(mediaInsertMessages.invalidUrlErrorMessage)
96
+ };
97
+ const [previewState, dispatch] = React.useReducer(previewStateReducer, INITIAL_PREVIEW_STATE);
98
+ const pasteFlag = React.useRef(false);
99
+ const {
100
+ onUploadButtonClickedAnalytics,
101
+ onUploadCommencedAnalytics,
102
+ onUploadSuccessAnalytics,
103
+ onUploadFailureAnalytics
104
+ } = useAnalyticsEvents(dispatchAnalyticsEvent);
105
+ const uploadExternalMedia = React.useCallback(async url => {
106
+ onUploadButtonClickedAnalytics();
107
+ dispatch({
108
+ type: 'loading'
109
+ });
110
+ const {
111
+ uploadMediaClientConfig,
112
+ uploadParams
113
+ } = mediaProvider;
114
+ if (!uploadMediaClientConfig) {
115
+ return;
116
+ }
117
+ const mediaClient = getMediaClient(uploadMediaClientConfig);
118
+ const collection = uploadParams === null || uploadParams === void 0 ? void 0 : uploadParams.collection;
119
+ onUploadCommencedAnalytics('url');
120
+ try {
121
+ const {
122
+ uploadableFileUpfrontIds,
123
+ dimensions,
124
+ mimeType
125
+ } = await mediaClient.file.uploadExternal(url, collection);
126
+ onUploadSuccessAnalytics('url');
127
+ dispatch({
128
+ type: 'success',
129
+ payload: {
130
+ id: uploadableFileUpfrontIds.id,
131
+ collection,
132
+ dimensions,
133
+ occurrenceKey: uploadableFileUpfrontIds.occurrenceKey,
134
+ fileMimeType: mimeType
135
+ }
136
+ });
137
+ } catch (e) {
138
+ if (typeof e === 'string' && e === 'Could not download remote file') {
139
+ // TODO: ED-26962 - Make sure this gets good unit test coverage with the actual media plugin.
140
+ // This hard coded error message could be changed at any
141
+ // point and we need a unit test to break to stop people changing it.
142
+ onUploadFailureAnalytics(e, 'url');
143
+ dispatch({
144
+ type: 'warning',
145
+ warning: e,
146
+ url
147
+ });
148
+ } else if (e instanceof Error) {
149
+ const message = 'Image preview fetch failed';
150
+ onUploadFailureAnalytics(message, 'url');
151
+ dispatch({
152
+ type: 'error',
153
+ error: message
154
+ });
155
+ } else {
156
+ onUploadFailureAnalytics('Unknown error', 'url');
157
+ dispatch({
158
+ type: 'error',
159
+ error: 'Unknown error'
160
+ });
161
+ }
162
+ }
163
+ }, [onUploadButtonClickedAnalytics, mediaProvider, onUploadCommencedAnalytics, onUploadSuccessAnalytics, onUploadFailureAnalytics]);
164
+ const onURLChange = React.useCallback(e => {
165
+ const url = e.currentTarget.value;
166
+ dispatch({
167
+ type: 'reset'
168
+ });
169
+ if (!isValidUrl(url)) {
170
+ return;
171
+ }
172
+ if (pasteFlag.current) {
173
+ pasteFlag.current = false;
174
+ uploadExternalMedia(url);
175
+ }
176
+ }, [uploadExternalMedia]);
177
+ const onPaste = React.useCallback((e, inputUrl) => {
178
+ // Note: this is a little weird, but the paste event will always be
179
+ // fired before the change event when pasting. We don't really want to
180
+ // duplicate logic by handling pastes separately to changes, so we're
181
+ // just noting paste occurred to then be handled in the onURLChange fn
182
+ // above. The one exception to this is where paste inputs exactly what was
183
+ // already in the input, in which case we want to ignore it.
184
+ if (e.clipboardData.getData('text') !== inputUrl) {
185
+ pasteFlag.current = true;
186
+ }
187
+ }, []);
188
+ const onInsert = React.useCallback(() => {
189
+ if (previewState.previewInfo) {
190
+ insertMediaSingle({
191
+ mediaState: previewState.previewInfo,
192
+ inputMethod: INPUT_METHOD.MEDIA_PICKER
193
+ });
194
+ }
195
+ closeMediaInsertPicker();
196
+ }, [closeMediaInsertPicker, insertMediaSingle, previewState.previewInfo]);
197
+ const onExternalInsert = React.useCallback(url => {
198
+ if (previewState.warning) {
199
+ insertExternalMediaSingle({
200
+ url,
201
+ alt: '',
202
+ inputMethod: INPUT_METHOD.MEDIA_PICKER
203
+ });
204
+ }
205
+ closeMediaInsertPicker();
206
+ }, [closeMediaInsertPicker, insertExternalMediaSingle, previewState.warning]);
207
+ const onInputKeyPress = React.useCallback(event => {
208
+ if (event && event.key === 'Esc') {
209
+ if (dispatchAnalyticsEvent) {
210
+ const payload = {
211
+ action: ACTION.CLOSED,
212
+ actionSubject: ACTION_SUBJECT.PICKER,
213
+ actionSubjectId: ACTION_SUBJECT_ID.PICKER_MEDIA,
214
+ eventType: EVENT_TYPE.UI,
215
+ attributes: {
216
+ exitMethod: INPUT_METHOD.KEYBOARD
217
+ }
218
+ };
219
+ dispatchAnalyticsEvent(payload);
220
+ }
221
+ closeMediaInsertPicker();
222
+ }
223
+ }, [dispatchAnalyticsEvent, closeMediaInsertPicker]);
224
+ const onCancel = React.useCallback(() => {
225
+ if (dispatchAnalyticsEvent) {
226
+ const payload = {
227
+ action: ACTION.CANCELLED,
228
+ actionSubject: ACTION_SUBJECT.PICKER,
229
+ actionSubjectId: ACTION_SUBJECT_ID.PICKER_MEDIA,
230
+ eventType: EVENT_TYPE.UI
231
+ };
232
+ dispatchAnalyticsEvent(payload);
233
+ }
234
+ closeMediaInsertPicker();
235
+ }, [closeMediaInsertPicker, dispatchAnalyticsEvent]);
236
+ return /*#__PURE__*/React.createElement(Form, {
237
+ onSubmit: ({
238
+ inputUrl
239
+ }, form) => {
240
+ // This can be triggered from an enter key event on the input even when
241
+ // the button is disabled, so we explicitly do nothing when in loading
242
+ // state.
243
+ if (previewState.isLoading || form.getState().invalid) {
244
+ return;
245
+ }
246
+ if (previewState.previewInfo) {
247
+ return onInsert();
248
+ }
249
+ if (previewState.warning) {
250
+ return onExternalInsert(inputUrl);
251
+ }
252
+ return uploadExternalMedia(inputUrl);
253
+ }
254
+ }, ({
255
+ formProps
256
+ }) =>
257
+ /*#__PURE__*/
258
+ // Ignored via go/ees005
259
+ // eslint-disable-next-line react/jsx-props-no-spreading
260
+ React.createElement(Box, _extends({
261
+ as: "form"
262
+ }, formProps, {
263
+ xcss: FormStyles
264
+ }), /*#__PURE__*/React.createElement(Stack, {
265
+ space: "space.150",
266
+ grow: "fill"
267
+ }, /*#__PURE__*/React.createElement(Field, {
268
+ "aria-required": true,
269
+ isRequired: true,
270
+ name: "inputUrl",
271
+ validate: value => value && isValidUrl(value) ? undefined : strings.invalidUrl
272
+ }, ({
273
+ fieldProps: {
274
+ value,
275
+ onChange,
276
+ ...rest
277
+ },
278
+ error,
279
+ meta
280
+ }) => /*#__PURE__*/React.createElement(Stack, {
281
+ space: "space.150",
282
+ grow: "fill"
283
+ }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(TextField
284
+ // Ignored via go/ees005
285
+ // eslint-disable-next-line react/jsx-props-no-spreading
286
+ , _extends({}, rest, {
287
+ value: value,
288
+ placeholder: strings.pasteLinkToUpload,
289
+ maxLength: MAX_URL_LENGTH,
290
+ onKeyPress: onInputKeyPress,
291
+ onPaste: event => onPaste(event, value),
292
+ onChange: value => {
293
+ onURLChange(value);
294
+ onChange(value);
295
+ }
296
+ })), /*#__PURE__*/React.createElement(MessageWrapper, null, error && /*#__PURE__*/React.createElement(ErrorMessage, null, /*#__PURE__*/React.createElement(Box, {
297
+ as: "span"
298
+ }, error)))), !previewState.previewInfo && !previewState.error && !previewState.warning && /*#__PURE__*/React.createElement(Flex, {
299
+ xcss: PreviewBoxStyles,
300
+ alignItems: "center",
301
+ justifyContent: "center"
302
+ }, /*#__PURE__*/React.createElement(Button, {
303
+ type: "submit",
304
+ isLoading: previewState.isLoading,
305
+ isDisabled: !!error || !meta.dirty,
306
+ iconBefore: () => /*#__PURE__*/React.createElement(ExpandIcon, {
307
+ label: "",
308
+ LEGACY_fallbackIcon: EditorFilePreviewIconLegacy
309
+ })
310
+ }, strings.loadPreview)))), previewState.previewInfo && /*#__PURE__*/React.createElement(Inline, {
311
+ alignInline: "center",
312
+ alignBlock: "center",
313
+ xcss: PreviewImageStyles,
314
+ space: "space.200"
315
+ }, /*#__PURE__*/React.createElement(MediaCard, {
316
+ attrs: previewState.previewInfo,
317
+ mediaProvider: mediaProvider
318
+ })), /*#__PURE__*/React.createElement(MessageWrapper, null, previewState.error && /*#__PURE__*/React.createElement(SectionMessage, {
319
+ appearance: "error"
320
+ }, strings.errorMessage), previewState.warning && /*#__PURE__*/React.createElement(SectionMessage, {
321
+ appearance: "warning"
322
+ }, strings.warning)), /*#__PURE__*/React.createElement(FormFooter, null, /*#__PURE__*/React.createElement(ButtonGroup, null, /*#__PURE__*/React.createElement(Button, {
323
+ appearance: "subtle",
324
+ onClick: onCancel
325
+ }, strings.cancel), /*#__PURE__*/React.createElement(Button, {
326
+ type: "submit",
327
+ appearance: "primary",
328
+ isDisabled: !previewState.previewInfo && !previewState.warning
329
+ }, strings.insert))))));
330
+ }
@@ -7,12 +7,14 @@ import { mediaInsertMessages } from '@atlaskit/editor-common/messages';
7
7
  import { PlainOutsideClickTargetRefContext, Popup, withOuterListeners } from '@atlaskit/editor-common/ui';
8
8
  import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
9
9
  import { akEditorFloatingDialogZIndex } from '@atlaskit/editor-shared-styles';
10
+ import { fg } from '@atlaskit/platform-feature-flags';
10
11
  import { Box } from '@atlaskit/primitives';
11
12
  import Tabs, { Tab, TabList, useTabPanel } from '@atlaskit/tabs';
12
13
  import { useFocusEditor } from './hooks/use-focus-editor';
13
14
  import { useUnholyAutofocus } from './hooks/use-unholy-autofocus';
14
15
  import { LocalMedia } from './LocalMedia';
15
16
  import { MediaFromURL } from './MediaFromURL';
17
+ import { MediaFromURLWithForm } from './MediaFromURLWithForm';
16
18
  import { MediaInsertWrapper } from './MediaInsertWrapper';
17
19
  const PopupWithListeners = withOuterListeners(Popup);
18
20
  const getDomRefFromSelection = (view, dispatchAnalyticsEvent) => {
@@ -150,7 +152,16 @@ export const MediaInsertPicker = ({
150
152
  },
151
153
  dispatchAnalyticsEvent: dispatchAnalyticsEvent,
152
154
  insertFile: insertFile
153
- })), /*#__PURE__*/React.createElement(CustomTabPanel, null, /*#__PURE__*/React.createElement(MediaFromURL, {
155
+ })), /*#__PURE__*/React.createElement(CustomTabPanel, null, fg('platform_editor_media_from_url_remove_form') ? /*#__PURE__*/React.createElement(MediaFromURL, {
156
+ mediaProvider: mediaProvider,
157
+ dispatchAnalyticsEvent: dispatchAnalyticsEvent,
158
+ closeMediaInsertPicker: () => {
159
+ closeMediaInsertPicker();
160
+ focusEditor();
161
+ },
162
+ insertMediaSingle: insertMediaSingle,
163
+ insertExternalMediaSingle: insertExternalMediaSingle
164
+ }) : /*#__PURE__*/React.createElement(MediaFromURLWithForm, {
154
165
  mediaProvider: mediaProvider,
155
166
  dispatchAnalyticsEvent: dispatchAnalyticsEvent,
156
167
  closeMediaInsertPicker: () => {
@@ -1,4 +1,3 @@
1
- import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
2
1
  import { pluginKey } from './plugin-key';
3
2
  export var ACTION_OPEN_POPUP = 'OPEN_POPUP';
4
3
  export var ACTION_CLOSE_POPUP = 'CLOSE_POPUP';
@@ -12,10 +11,6 @@ var setPopupMeta = function setPopupMeta(_ref) {
12
11
  });
13
12
  };
14
13
  export var showMediaInsertPopup = function showMediaInsertPopup(tr, mountInfo) {
15
- // Log exposure here but don't actually switch anything on it
16
- editorExperiment('add-media-from-url', true, {
17
- exposure: true
18
- });
19
14
  return setPopupMeta({
20
15
  type: ACTION_OPEN_POPUP,
21
16
  mountInfo: mountInfo,
@@ -280,11 +280,9 @@ export function MediaFromURL(_ref) {
280
280
  /*#__PURE__*/
281
281
  // Ignored via go/ees005
282
282
  // eslint-disable-next-line react/jsx-props-no-spreading
283
- React.createElement(Box, _extends({
284
- as: "form"
285
- }, formProps, {
283
+ React.createElement(Box, {
286
284
  xcss: FormStyles
287
- }), /*#__PURE__*/React.createElement(Stack, {
285
+ }, /*#__PURE__*/React.createElement(Stack, {
288
286
  space: "space.150",
289
287
  grow: "fill"
290
288
  }, /*#__PURE__*/React.createElement(Field, {
@@ -318,6 +316,12 @@ export function MediaFromURL(_ref) {
318
316
  onChange: function onChange(value) {
319
317
  onURLChange(value);
320
318
  _onChange(value);
319
+ },
320
+ onKeyDown: function onKeyDown(e) {
321
+ if (e.key === 'Enter') {
322
+ e.preventDefault();
323
+ formProps.onSubmit();
324
+ }
321
325
  }
322
326
  })), /*#__PURE__*/React.createElement(MessageWrapper, null, error && /*#__PURE__*/React.createElement(ErrorMessage, null, /*#__PURE__*/React.createElement(Box, {
323
327
  as: "span"
@@ -326,7 +330,10 @@ export function MediaFromURL(_ref) {
326
330
  alignItems: "center",
327
331
  justifyContent: "center"
328
332
  }, /*#__PURE__*/React.createElement(Button, {
329
- type: "submit",
333
+ type: "button",
334
+ onClick: function onClick() {
335
+ return formProps.onSubmit();
336
+ },
330
337
  isLoading: previewState.isLoading,
331
338
  isDisabled: !!error || !meta.dirty,
332
339
  iconBefore: function iconBefore() {
@@ -352,9 +359,12 @@ export function MediaFromURL(_ref) {
352
359
  appearance: "subtle",
353
360
  onClick: onCancel
354
361
  }, strings.cancel), /*#__PURE__*/React.createElement(Button, {
355
- type: "submit",
362
+ type: "button",
356
363
  appearance: "primary",
357
- isDisabled: !previewState.previewInfo && !previewState.warning
364
+ isDisabled: !previewState.previewInfo && !previewState.warning,
365
+ onClick: function onClick() {
366
+ return formProps.onSubmit();
367
+ }
358
368
  }, strings.insert)))))
359
369
  );
360
370
  });