@atlaskit/editor-plugin-paste-options-toolbar 9.1.7 → 9.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @atlaskit/editor-plugin-paste-options-toolbar
2
2
 
3
+ ## 9.1.9
4
+
5
+ ### Patch Changes
6
+
7
+ - [`194f00cfcef60`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/194f00cfcef60) -
8
+ EDITOR-6197 Fire manual experiment exposure for paste actions menu experiment
9
+
10
+ ## 9.1.8
11
+
12
+ ### Patch Changes
13
+
14
+ - [`fb9bbd5719238`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/fb9bbd5719238) -
15
+ EDITOR-6193 Correcting `visibleAiActions` attribute of paste actions menu which was not being
16
+ populated.
17
+
3
18
  ## 9.1.7
4
19
 
5
20
  ### Patch Changes
@@ -7,9 +7,12 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.pasteOptionsToolbarPlugin = void 0;
8
8
  var _react = _interopRequireWildcard(require("react"));
9
9
  var _hooks = require("@atlaskit/editor-common/hooks");
10
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
11
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
10
12
  var _commands = require("./editor-commands/commands");
11
13
  var _main = require("./pm-plugins/main");
12
14
  var _types = require("./types/types");
15
+ var _exposure = require("./ui/on-paste-actions-menu/exposure");
13
16
  var _PasteActionsMenu = require("./ui/on-paste-actions-menu/PasteActionsMenu");
14
17
  var _PasteMenuComponents = require("./ui/on-paste-actions-menu/PasteMenuComponents");
15
18
  var _toolbar = require("./ui/toolbar");
@@ -19,7 +22,7 @@ var pasteOptionsToolbarPlugin = exports.pasteOptionsToolbarPlugin = function pas
19
22
  var config = _ref.config,
20
23
  api = _ref.api;
21
24
  var editorAnalyticsAPI = api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
22
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
25
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_paste_actions_menu', 'isEnabled', true)) {
23
26
  var _api$uiControlRegistr;
24
27
  api === null || api === void 0 || (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 || _api$uiControlRegistr.actions.register((0, _PasteMenuComponents.getPasteMenuComponents)({
25
28
  api: api
@@ -33,7 +36,7 @@ var pasteOptionsToolbarPlugin = exports.pasteOptionsToolbarPlugin = function pas
33
36
  plugin: function plugin(_ref2) {
34
37
  var dispatch = _ref2.dispatch;
35
38
  return (0, _main.createPlugin)(dispatch, {
36
- useNewPasteMenu: config === null || config === void 0 ? void 0 : config.usePopupBasedPasteActionsMenu
39
+ useNewPasteMenu: (config === null || config === void 0 ? void 0 : config.usePopupBasedPasteActionsMenu) && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_paste_actions_menu', 'isEnabled', true)
37
40
  });
38
41
  }
39
42
  }];
@@ -66,7 +69,7 @@ var pasteOptionsToolbarPlugin = exports.pasteOptionsToolbarPlugin = function pas
66
69
  },
67
70
  pluginsOptions: {
68
71
  floatingToolbar: function floatingToolbar(state, intl) {
69
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
72
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_paste_actions_menu', 'isEnabled', true)) {
70
73
  return;
71
74
  }
72
75
  var pastePluginState = _types.pasteOptionsPluginKey.getState(state);
@@ -81,7 +84,7 @@ var pasteOptionsToolbarPlugin = exports.pasteOptionsToolbarPlugin = function pas
81
84
  popupsMountPoint = _ref3.popupsMountPoint,
82
85
  popupsBoundariesElement = _ref3.popupsBoundariesElement,
83
86
  popupsScrollableElement = _ref3.popupsScrollableElement;
84
- if (!(config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) || !editorView) {
87
+ if (!(config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_paste_actions_menu', 'isEnabled', true)) || !editorView) {
85
88
  return null;
86
89
  }
87
90
  return /*#__PURE__*/_react.default.createElement(_PasteActionsMenu.PasteActionsMenu, {
@@ -102,7 +105,11 @@ var pasteOptionsToolbarPlugin = exports.pasteOptionsToolbarPlugin = function pas
102
105
  }),
103
106
  lastContentPasted = _useSharedPluginState.lastContentPasted;
104
107
  (0, _react.useEffect)(function () {
105
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
108
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && (0, _platformFeatureFlags.fg)('platform_editor_paste_actions_menu_exposure')) {
109
+ var _lastContentPasted$te, _lastContentPasted$te2;
110
+ (0, _exposure.firePasteActionsMenuExperimentExposure)((_lastContentPasted$te = lastContentPasted === null || lastContentPasted === void 0 || (_lastContentPasted$te2 = lastContentPasted.text) === null || _lastContentPasted$te2 === void 0 ? void 0 : _lastContentPasted$te2.length) !== null && _lastContentPasted$te !== void 0 ? _lastContentPasted$te : 0, editorView.state, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.pasteStartPos, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.pasteEndPos, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.text);
111
+ }
112
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_paste_actions_menu', 'isEnabled', true)) {
106
113
  return;
107
114
  }
108
115
  if (!lastContentPasted) {
@@ -225,7 +225,7 @@ function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElem
225
225
  };
226
226
  }
227
227
  var PasteActionsMenu = exports.PasteActionsMenu = function PasteActionsMenu(_ref) {
228
- var _api$analytics, _api$uiControlRegistr, _api$uiControlRegistr2, _api$uiControlRegistr3, _api$uiControlRegistr4;
228
+ var _api$analytics, _api$uiControlRegistr, _api$uiControlRegistr2;
229
229
  var api = _ref.api,
230
230
  editorView = _ref.editorView,
231
231
  mountTo = _ref.mountTo,
@@ -279,22 +279,6 @@ var PasteActionsMenu = exports.PasteActionsMenu = function PasteActionsMenu(_ref
279
279
  isToolbarShown = _useSharedPluginState2.showToolbar,
280
280
  pasteStartPos = _useSharedPluginState2.pasteStartPos,
281
281
  pasteEndPos = _useSharedPluginState2.pasteEndPos;
282
- var aiSurfaceComponents = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents('ai-paste-menu')) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
283
- var visibleAiActionKeys = (0, _hasVisibleButton.getVisibleKeys)(aiSurfaceComponents, ['button', 'menu-item']);
284
- (0, _react.useEffect)(function () {
285
- if (!prevShowToolbarRef.current && isToolbarShown) {
286
- editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.fireAnalyticsEvent({
287
- action: _analytics.ACTION.OPENED,
288
- actionSubject: _analytics.ACTION_SUBJECT.PASTE_ACTIONS_MENU,
289
- eventType: _analytics.EVENT_TYPE.UI,
290
- attributes: {
291
- visibleAiActions: visibleAiActionKeys
292
- }
293
- });
294
- }
295
- prevShowToolbarRef.current = isToolbarShown;
296
- // eslint-disable-next-line react-hooks/exhaustive-deps
297
- }, [isToolbarShown, editorAnalyticsAPI]);
298
282
  var preventEditorFocusLoss = (0, _react.useCallback)(function (e) {
299
283
  e.preventDefault();
300
284
  }, []);
@@ -326,22 +310,38 @@ var PasteActionsMenu = exports.PasteActionsMenu = function PasteActionsMenu(_ref
326
310
  // onPositionCalculated with fresh viewport coordinates.
327
311
  var overflowScrollParent = isToolbarShown ? (0, _ui.findOverflowScrollParent)(editorView.dom) : false;
328
312
  var effectiveScrollableElement = overflowScrollParent || scrollableElement;
329
- var pasteMenuComponents = (_api$uiControlRegistr3 = api === null || api === void 0 || (_api$uiControlRegistr4 = api.uiControlRegistry) === null || _api$uiControlRegistr4 === void 0 ? void 0 : _api$uiControlRegistr4.actions.getComponents(_toolbar.PASTE_MENU.key)) !== null && _api$uiControlRegistr3 !== void 0 ? _api$uiControlRegistr3 : [];
313
+ var pasteMenuComponents = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(_toolbar.PASTE_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
330
314
  var anyComponentVisible = (0, _hasVisibleButton.hasVisibleButton)(pasteMenuComponents);
331
315
 
332
- // Two positioning modes:
333
- // 1. Inline: no AI actions visible — menu appears to the right of the cursor,
334
- // vertically centered with the text line.
335
- // 2. Block-anchored: AI actions are visible — menu appears at the right edge
336
- // of the content block, aligned with paste start.
337
- var hasVisibleAiActions = (0, _hasVisibleButton.getVisibleKeys)(
338
316
  // eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- pasteMenuComponents changes by reference each render; filter is small (< 10 items)
339
- pasteMenuComponents.filter(function (c) {
317
+ var aiMenuItems = pasteMenuComponents.filter(function (c) {
340
318
  var _c$parents;
341
319
  return c.type === 'menu-item' && ((_c$parents = c.parents) === null || _c$parents === void 0 ? void 0 : _c$parents.some(function (p) {
342
320
  return p.key === _toolbar.AI_PASTE_MENU_SECTION.key;
343
321
  }));
344
- }), ['menu-item']).length > 0;
322
+ });
323
+ var visibleAiActionKeys = (0, _hasVisibleButton.getVisibleKeys)(aiMenuItems, ['menu-item']);
324
+
325
+ // Two positioning modes:
326
+ // 1. Inline: no AI actions visible — menu appears to the right of the cursor,
327
+ // vertically centered with the text line.
328
+ // 2. Block-anchored: AI actions are visible — menu appears at the right edge
329
+ // of the content block, aligned with paste start.
330
+ var hasVisibleAiActions = visibleAiActionKeys.length > 0;
331
+ (0, _react.useEffect)(function () {
332
+ if (!prevShowToolbarRef.current && isToolbarShown) {
333
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.fireAnalyticsEvent({
334
+ action: _analytics.ACTION.OPENED,
335
+ actionSubject: _analytics.ACTION_SUBJECT.PASTE_ACTIONS_MENU,
336
+ eventType: _analytics.EVENT_TYPE.UI,
337
+ attributes: {
338
+ visibleAiActions: visibleAiActionKeys
339
+ }
340
+ });
341
+ }
342
+ prevShowToolbarRef.current = isToolbarShown;
343
+ // eslint-disable-next-line react-hooks/exhaustive-deps
344
+ }, [isToolbarShown, editorAnalyticsAPI]);
345
345
  var useInlinePosition = !hasVisibleAiActions;
346
346
  if (!isToolbarShown) {
347
347
  return null;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.firePasteActionsMenuExperimentExposure = void 0;
7
+ var _expVal = require("@atlaskit/tmp-editor-statsig/expVal");
8
+ var isNotProse = function isNotProse(text) {
9
+ var trimmed = text.trim();
10
+ if (!trimmed) {
11
+ return false;
12
+ }
13
+
14
+ // Check each character: if we find whitespace it's prose-like,
15
+ // if we find a non-ASCII character it's likely CJK/Thai/etc.
16
+ for (var i = 0; i < trimmed.length; i++) {
17
+ var code = trimmed.charCodeAt(i);
18
+ // Whitespace (space, tab, newline, etc.) → prose-like
19
+ if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {
20
+ return false;
21
+ }
22
+ // Non-ASCII character → likely a non-Latin script (CJK, Thai, etc.)
23
+ if (code > 0x7f) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ // No whitespace and all ASCII → URL, token, path, etc.
29
+ return true;
30
+ };
31
+
32
+ // Manual exposure event for `platform_editor_paste_actions_menu`. Due to the fact that as part of this experiment
33
+ // the paste menu was completely redesigned, it was very difficult to ensure that an exposure event fires accurately
34
+ // for both control and test cohorts without executing code paths for both menus.
35
+ // This manual exposure event executes all criteria for showing AI buttons and fires the exposure manually in a code path that
36
+ // is guaranteed to execute on both control and test.
37
+ var firePasteActionsMenuExperimentExposure = exports.firePasteActionsMenuExperimentExposure = function firePasteActionsMenuExperimentExposure(contentLength, state, pasteStartPos, pasteEndPos, pastedText) {
38
+ if (contentLength < 100 || !pasteStartPos || !pasteEndPos || !pastedText) {
39
+ return;
40
+ }
41
+ if (isNotProse(pastedText)) {
42
+ return;
43
+ }
44
+ try {
45
+ var $pos = state.doc.resolve(pasteStartPos);
46
+ var pasteAncestorNodeNames = [];
47
+ for (var depth = $pos.depth; depth > 0; depth--) {
48
+ // Only include an ancestor if the entire pasted range is contained within it.
49
+ // This prevents nodes like 'heading' from being flagged as ancestors when the
50
+ // pasted content starts in a heading but extends beyond it (e.g. heading + paragraph).
51
+ if (pasteEndPos <= $pos.end(depth)) {
52
+ pasteAncestorNodeNames.push($pos.node(depth).type.name);
53
+ }
54
+ }
55
+ var isInExcludedNode = pasteAncestorNodeNames.some(function (name) {
56
+ return ['codeBlock', 'heading'].includes(name);
57
+ });
58
+ if (!isInExcludedNode) {
59
+ (0, _expVal.expVal)('platform_editor_paste_actions_menu', 'isEnabled', false);
60
+ }
61
+ } catch (_unused) {
62
+ // pasteStartPos may be out of bounds if the document changed between
63
+ // when the paste was recorded and when this effect fires.
64
+ return;
65
+ }
66
+ };
@@ -1,8 +1,11 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
3
+ import { fg } from '@atlaskit/platform-feature-flags';
4
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
3
5
  import { hideToolbar, showToolbar } from './editor-commands/commands';
4
6
  import { createPlugin } from './pm-plugins/main';
5
7
  import { pasteOptionsPluginKey, ToolbarDropdownOption } from './types/types';
8
+ import { firePasteActionsMenuExperimentExposure } from './ui/on-paste-actions-menu/exposure';
6
9
  import { PasteActionsMenu } from './ui/on-paste-actions-menu/PasteActionsMenu';
7
10
  import { getPasteMenuComponents } from './ui/on-paste-actions-menu/PasteMenuComponents';
8
11
  import { buildToolbar, isToolbarVisible } from './ui/toolbar';
@@ -12,7 +15,7 @@ export const pasteOptionsToolbarPlugin = ({
12
15
  }) => {
13
16
  var _api$analytics;
14
17
  const editorAnalyticsAPI = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
15
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
18
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)) {
16
19
  var _api$uiControlRegistr;
17
20
  api === null || api === void 0 ? void 0 : (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 ? void 0 : _api$uiControlRegistr.actions.register(getPasteMenuComponents({
18
21
  api
@@ -26,7 +29,7 @@ export const pasteOptionsToolbarPlugin = ({
26
29
  plugin: ({
27
30
  dispatch
28
31
  }) => createPlugin(dispatch, {
29
- useNewPasteMenu: config === null || config === void 0 ? void 0 : config.usePopupBasedPasteActionsMenu
32
+ useNewPasteMenu: (config === null || config === void 0 ? void 0 : config.usePopupBasedPasteActionsMenu) && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)
30
33
  })
31
34
  }];
32
35
  },
@@ -58,7 +61,7 @@ export const pasteOptionsToolbarPlugin = ({
58
61
  },
59
62
  pluginsOptions: {
60
63
  floatingToolbar(state, intl) {
61
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
64
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)) {
62
65
  return;
63
66
  }
64
67
  const pastePluginState = pasteOptionsPluginKey.getState(state);
@@ -74,7 +77,7 @@ export const pasteOptionsToolbarPlugin = ({
74
77
  popupsBoundariesElement,
75
78
  popupsScrollableElement
76
79
  }) {
77
- if (!(config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) || !editorView) {
80
+ if (!(config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)) || !editorView) {
78
81
  return null;
79
82
  }
80
83
  return /*#__PURE__*/React.createElement(PasteActionsMenu, {
@@ -97,7 +100,11 @@ export const pasteOptionsToolbarPlugin = ({
97
100
  };
98
101
  });
99
102
  useEffect(() => {
100
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
103
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && fg('platform_editor_paste_actions_menu_exposure')) {
104
+ var _lastContentPasted$te, _lastContentPasted$te2;
105
+ firePasteActionsMenuExperimentExposure((_lastContentPasted$te = lastContentPasted === null || lastContentPasted === void 0 ? void 0 : (_lastContentPasted$te2 = lastContentPasted.text) === null || _lastContentPasted$te2 === void 0 ? void 0 : _lastContentPasted$te2.length) !== null && _lastContentPasted$te !== void 0 ? _lastContentPasted$te : 0, editorView.state, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.pasteStartPos, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.pasteEndPos, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.text);
106
+ }
107
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)) {
101
108
  return;
102
109
  }
103
110
  if (!lastContentPasted) {
@@ -215,7 +215,7 @@ export const PasteActionsMenu = ({
215
215
  boundariesElement,
216
216
  scrollableElement
217
217
  }) => {
218
- var _api$analytics, _api$uiControlRegistr, _api$uiControlRegistr2, _api$uiControlRegistr3, _api$uiControlRegistr4;
218
+ var _api$analytics, _api$uiControlRegistr, _api$uiControlRegistr2;
219
219
  const editorAnalyticsAPI = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
220
220
  const {
221
221
  lastContentPasted
@@ -266,22 +266,6 @@ export const PasteActionsMenu = ({
266
266
  pasteEndPos: (_pluginState$pasteEnd = pluginState === null || pluginState === void 0 ? void 0 : pluginState.pasteEndPos) !== null && _pluginState$pasteEnd !== void 0 ? _pluginState$pasteEnd : 0
267
267
  };
268
268
  });
269
- const aiSurfaceComponents = (_api$uiControlRegistr = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents('ai-paste-menu')) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
270
- const visibleAiActionKeys = getVisibleKeys(aiSurfaceComponents, ['button', 'menu-item']);
271
- useEffect(() => {
272
- if (!prevShowToolbarRef.current && isToolbarShown) {
273
- editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.fireAnalyticsEvent({
274
- action: ACTION.OPENED,
275
- actionSubject: ACTION_SUBJECT.PASTE_ACTIONS_MENU,
276
- eventType: EVENT_TYPE.UI,
277
- attributes: {
278
- visibleAiActions: visibleAiActionKeys
279
- }
280
- });
281
- }
282
- prevShowToolbarRef.current = isToolbarShown;
283
- // eslint-disable-next-line react-hooks/exhaustive-deps
284
- }, [isToolbarShown, editorAnalyticsAPI]);
285
269
  const preventEditorFocusLoss = useCallback(e => {
286
270
  e.preventDefault();
287
271
  }, []);
@@ -313,20 +297,36 @@ export const PasteActionsMenu = ({
313
297
  // onPositionCalculated with fresh viewport coordinates.
314
298
  const overflowScrollParent = isToolbarShown ? findOverflowScrollParent(editorView.dom) : false;
315
299
  const effectiveScrollableElement = overflowScrollParent || scrollableElement;
316
- const pasteMenuComponents = (_api$uiControlRegistr3 = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr4 = api.uiControlRegistry) === null || _api$uiControlRegistr4 === void 0 ? void 0 : _api$uiControlRegistr4.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr3 !== void 0 ? _api$uiControlRegistr3 : [];
300
+ const pasteMenuComponents = (_api$uiControlRegistr = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
317
301
  const anyComponentVisible = hasVisibleButton(pasteMenuComponents);
318
302
 
303
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- pasteMenuComponents changes by reference each render; filter is small (< 10 items)
304
+ const aiMenuItems = pasteMenuComponents.filter(c => {
305
+ var _c$parents;
306
+ return c.type === 'menu-item' && ((_c$parents = c.parents) === null || _c$parents === void 0 ? void 0 : _c$parents.some(p => p.key === AI_PASTE_MENU_SECTION.key));
307
+ });
308
+ const visibleAiActionKeys = getVisibleKeys(aiMenuItems, ['menu-item']);
309
+
319
310
  // Two positioning modes:
320
311
  // 1. Inline: no AI actions visible — menu appears to the right of the cursor,
321
312
  // vertically centered with the text line.
322
313
  // 2. Block-anchored: AI actions are visible — menu appears at the right edge
323
314
  // of the content block, aligned with paste start.
324
- const hasVisibleAiActions = getVisibleKeys(
325
- // eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- pasteMenuComponents changes by reference each render; filter is small (< 10 items)
326
- pasteMenuComponents.filter(c => {
327
- var _c$parents;
328
- return c.type === 'menu-item' && ((_c$parents = c.parents) === null || _c$parents === void 0 ? void 0 : _c$parents.some(p => p.key === AI_PASTE_MENU_SECTION.key));
329
- }), ['menu-item']).length > 0;
315
+ const hasVisibleAiActions = visibleAiActionKeys.length > 0;
316
+ useEffect(() => {
317
+ if (!prevShowToolbarRef.current && isToolbarShown) {
318
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.fireAnalyticsEvent({
319
+ action: ACTION.OPENED,
320
+ actionSubject: ACTION_SUBJECT.PASTE_ACTIONS_MENU,
321
+ eventType: EVENT_TYPE.UI,
322
+ attributes: {
323
+ visibleAiActions: visibleAiActionKeys
324
+ }
325
+ });
326
+ }
327
+ prevShowToolbarRef.current = isToolbarShown;
328
+ // eslint-disable-next-line react-hooks/exhaustive-deps
329
+ }, [isToolbarShown, editorAnalyticsAPI]);
330
330
  const useInlinePosition = !hasVisibleAiActions;
331
331
  if (!isToolbarShown) {
332
332
  return null;
@@ -0,0 +1,58 @@
1
+ import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
2
+ const isNotProse = text => {
3
+ const trimmed = text.trim();
4
+ if (!trimmed) {
5
+ return false;
6
+ }
7
+
8
+ // Check each character: if we find whitespace it's prose-like,
9
+ // if we find a non-ASCII character it's likely CJK/Thai/etc.
10
+ for (let i = 0; i < trimmed.length; i++) {
11
+ const code = trimmed.charCodeAt(i);
12
+ // Whitespace (space, tab, newline, etc.) → prose-like
13
+ if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {
14
+ return false;
15
+ }
16
+ // Non-ASCII character → likely a non-Latin script (CJK, Thai, etc.)
17
+ if (code > 0x7f) {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ // No whitespace and all ASCII → URL, token, path, etc.
23
+ return true;
24
+ };
25
+
26
+ // Manual exposure event for `platform_editor_paste_actions_menu`. Due to the fact that as part of this experiment
27
+ // the paste menu was completely redesigned, it was very difficult to ensure that an exposure event fires accurately
28
+ // for both control and test cohorts without executing code paths for both menus.
29
+ // This manual exposure event executes all criteria for showing AI buttons and fires the exposure manually in a code path that
30
+ // is guaranteed to execute on both control and test.
31
+ export const firePasteActionsMenuExperimentExposure = (contentLength, state, pasteStartPos, pasteEndPos, pastedText) => {
32
+ if (contentLength < 100 || !pasteStartPos || !pasteEndPos || !pastedText) {
33
+ return;
34
+ }
35
+ if (isNotProse(pastedText)) {
36
+ return;
37
+ }
38
+ try {
39
+ const $pos = state.doc.resolve(pasteStartPos);
40
+ const pasteAncestorNodeNames = [];
41
+ for (let depth = $pos.depth; depth > 0; depth--) {
42
+ // Only include an ancestor if the entire pasted range is contained within it.
43
+ // This prevents nodes like 'heading' from being flagged as ancestors when the
44
+ // pasted content starts in a heading but extends beyond it (e.g. heading + paragraph).
45
+ if (pasteEndPos <= $pos.end(depth)) {
46
+ pasteAncestorNodeNames.push($pos.node(depth).type.name);
47
+ }
48
+ }
49
+ const isInExcludedNode = pasteAncestorNodeNames.some(name => ['codeBlock', 'heading'].includes(name));
50
+ if (!isInExcludedNode) {
51
+ expVal('platform_editor_paste_actions_menu', 'isEnabled', false);
52
+ }
53
+ } catch {
54
+ // pasteStartPos may be out of bounds if the document changed between
55
+ // when the paste was recorded and when this effect fires.
56
+ return;
57
+ }
58
+ };
@@ -1,8 +1,11 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
3
+ import { fg } from '@atlaskit/platform-feature-flags';
4
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
3
5
  import { hideToolbar, showToolbar } from './editor-commands/commands';
4
6
  import { createPlugin } from './pm-plugins/main';
5
7
  import { pasteOptionsPluginKey, ToolbarDropdownOption } from './types/types';
8
+ import { firePasteActionsMenuExperimentExposure } from './ui/on-paste-actions-menu/exposure';
6
9
  import { PasteActionsMenu } from './ui/on-paste-actions-menu/PasteActionsMenu';
7
10
  import { getPasteMenuComponents } from './ui/on-paste-actions-menu/PasteMenuComponents';
8
11
  import { buildToolbar, isToolbarVisible } from './ui/toolbar';
@@ -11,7 +14,7 @@ export var pasteOptionsToolbarPlugin = function pasteOptionsToolbarPlugin(_ref)
11
14
  var config = _ref.config,
12
15
  api = _ref.api;
13
16
  var editorAnalyticsAPI = api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
14
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
17
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)) {
15
18
  var _api$uiControlRegistr;
16
19
  api === null || api === void 0 || (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 || _api$uiControlRegistr.actions.register(getPasteMenuComponents({
17
20
  api: api
@@ -25,7 +28,7 @@ export var pasteOptionsToolbarPlugin = function pasteOptionsToolbarPlugin(_ref)
25
28
  plugin: function plugin(_ref2) {
26
29
  var dispatch = _ref2.dispatch;
27
30
  return createPlugin(dispatch, {
28
- useNewPasteMenu: config === null || config === void 0 ? void 0 : config.usePopupBasedPasteActionsMenu
31
+ useNewPasteMenu: (config === null || config === void 0 ? void 0 : config.usePopupBasedPasteActionsMenu) && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)
29
32
  });
30
33
  }
31
34
  }];
@@ -58,7 +61,7 @@ export var pasteOptionsToolbarPlugin = function pasteOptionsToolbarPlugin(_ref)
58
61
  },
59
62
  pluginsOptions: {
60
63
  floatingToolbar: function floatingToolbar(state, intl) {
61
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
64
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)) {
62
65
  return;
63
66
  }
64
67
  var pastePluginState = pasteOptionsPluginKey.getState(state);
@@ -73,7 +76,7 @@ export var pasteOptionsToolbarPlugin = function pasteOptionsToolbarPlugin(_ref)
73
76
  popupsMountPoint = _ref3.popupsMountPoint,
74
77
  popupsBoundariesElement = _ref3.popupsBoundariesElement,
75
78
  popupsScrollableElement = _ref3.popupsScrollableElement;
76
- if (!(config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) || !editorView) {
79
+ if (!(config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)) || !editorView) {
77
80
  return null;
78
81
  }
79
82
  return /*#__PURE__*/React.createElement(PasteActionsMenu, {
@@ -94,7 +97,11 @@ export var pasteOptionsToolbarPlugin = function pasteOptionsToolbarPlugin(_ref)
94
97
  }),
95
98
  lastContentPasted = _useSharedPluginState.lastContentPasted;
96
99
  useEffect(function () {
97
- if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
100
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && fg('platform_editor_paste_actions_menu_exposure')) {
101
+ var _lastContentPasted$te, _lastContentPasted$te2;
102
+ firePasteActionsMenuExperimentExposure((_lastContentPasted$te = lastContentPasted === null || lastContentPasted === void 0 || (_lastContentPasted$te2 = lastContentPasted.text) === null || _lastContentPasted$te2 === void 0 ? void 0 : _lastContentPasted$te2.length) !== null && _lastContentPasted$te !== void 0 ? _lastContentPasted$te : 0, editorView.state, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.pasteStartPos, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.pasteEndPos, lastContentPasted === null || lastContentPasted === void 0 ? void 0 : lastContentPasted.text);
103
+ }
104
+ if (config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu && expValEqualsNoExposure('platform_editor_paste_actions_menu', 'isEnabled', true)) {
98
105
  return;
99
106
  }
100
107
  if (!lastContentPasted) {
@@ -210,7 +210,7 @@ export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, tar
210
210
  };
211
211
  }
212
212
  export var PasteActionsMenu = function PasteActionsMenu(_ref) {
213
- var _api$analytics, _api$uiControlRegistr, _api$uiControlRegistr2, _api$uiControlRegistr3, _api$uiControlRegistr4;
213
+ var _api$analytics, _api$uiControlRegistr, _api$uiControlRegistr2;
214
214
  var api = _ref.api,
215
215
  editorView = _ref.editorView,
216
216
  mountTo = _ref.mountTo,
@@ -264,22 +264,6 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
264
264
  isToolbarShown = _useSharedPluginState2.showToolbar,
265
265
  pasteStartPos = _useSharedPluginState2.pasteStartPos,
266
266
  pasteEndPos = _useSharedPluginState2.pasteEndPos;
267
- var aiSurfaceComponents = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents('ai-paste-menu')) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
268
- var visibleAiActionKeys = getVisibleKeys(aiSurfaceComponents, ['button', 'menu-item']);
269
- useEffect(function () {
270
- if (!prevShowToolbarRef.current && isToolbarShown) {
271
- editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.fireAnalyticsEvent({
272
- action: ACTION.OPENED,
273
- actionSubject: ACTION_SUBJECT.PASTE_ACTIONS_MENU,
274
- eventType: EVENT_TYPE.UI,
275
- attributes: {
276
- visibleAiActions: visibleAiActionKeys
277
- }
278
- });
279
- }
280
- prevShowToolbarRef.current = isToolbarShown;
281
- // eslint-disable-next-line react-hooks/exhaustive-deps
282
- }, [isToolbarShown, editorAnalyticsAPI]);
283
267
  var preventEditorFocusLoss = useCallback(function (e) {
284
268
  e.preventDefault();
285
269
  }, []);
@@ -311,22 +295,38 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
311
295
  // onPositionCalculated with fresh viewport coordinates.
312
296
  var overflowScrollParent = isToolbarShown ? findOverflowScrollParent(editorView.dom) : false;
313
297
  var effectiveScrollableElement = overflowScrollParent || scrollableElement;
314
- var pasteMenuComponents = (_api$uiControlRegistr3 = api === null || api === void 0 || (_api$uiControlRegistr4 = api.uiControlRegistry) === null || _api$uiControlRegistr4 === void 0 ? void 0 : _api$uiControlRegistr4.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr3 !== void 0 ? _api$uiControlRegistr3 : [];
298
+ var pasteMenuComponents = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
315
299
  var anyComponentVisible = hasVisibleButton(pasteMenuComponents);
316
300
 
317
- // Two positioning modes:
318
- // 1. Inline: no AI actions visible — menu appears to the right of the cursor,
319
- // vertically centered with the text line.
320
- // 2. Block-anchored: AI actions are visible — menu appears at the right edge
321
- // of the content block, aligned with paste start.
322
- var hasVisibleAiActions = getVisibleKeys(
323
301
  // eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- pasteMenuComponents changes by reference each render; filter is small (< 10 items)
324
- pasteMenuComponents.filter(function (c) {
302
+ var aiMenuItems = pasteMenuComponents.filter(function (c) {
325
303
  var _c$parents;
326
304
  return c.type === 'menu-item' && ((_c$parents = c.parents) === null || _c$parents === void 0 ? void 0 : _c$parents.some(function (p) {
327
305
  return p.key === AI_PASTE_MENU_SECTION.key;
328
306
  }));
329
- }), ['menu-item']).length > 0;
307
+ });
308
+ var visibleAiActionKeys = getVisibleKeys(aiMenuItems, ['menu-item']);
309
+
310
+ // Two positioning modes:
311
+ // 1. Inline: no AI actions visible — menu appears to the right of the cursor,
312
+ // vertically centered with the text line.
313
+ // 2. Block-anchored: AI actions are visible — menu appears at the right edge
314
+ // of the content block, aligned with paste start.
315
+ var hasVisibleAiActions = visibleAiActionKeys.length > 0;
316
+ useEffect(function () {
317
+ if (!prevShowToolbarRef.current && isToolbarShown) {
318
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.fireAnalyticsEvent({
319
+ action: ACTION.OPENED,
320
+ actionSubject: ACTION_SUBJECT.PASTE_ACTIONS_MENU,
321
+ eventType: EVENT_TYPE.UI,
322
+ attributes: {
323
+ visibleAiActions: visibleAiActionKeys
324
+ }
325
+ });
326
+ }
327
+ prevShowToolbarRef.current = isToolbarShown;
328
+ // eslint-disable-next-line react-hooks/exhaustive-deps
329
+ }, [isToolbarShown, editorAnalyticsAPI]);
330
330
  var useInlinePosition = !hasVisibleAiActions;
331
331
  if (!isToolbarShown) {
332
332
  return null;
@@ -0,0 +1,60 @@
1
+ import { expVal } from '@atlaskit/tmp-editor-statsig/expVal';
2
+ var isNotProse = function isNotProse(text) {
3
+ var trimmed = text.trim();
4
+ if (!trimmed) {
5
+ return false;
6
+ }
7
+
8
+ // Check each character: if we find whitespace it's prose-like,
9
+ // if we find a non-ASCII character it's likely CJK/Thai/etc.
10
+ for (var i = 0; i < trimmed.length; i++) {
11
+ var code = trimmed.charCodeAt(i);
12
+ // Whitespace (space, tab, newline, etc.) → prose-like
13
+ if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {
14
+ return false;
15
+ }
16
+ // Non-ASCII character → likely a non-Latin script (CJK, Thai, etc.)
17
+ if (code > 0x7f) {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ // No whitespace and all ASCII → URL, token, path, etc.
23
+ return true;
24
+ };
25
+
26
+ // Manual exposure event for `platform_editor_paste_actions_menu`. Due to the fact that as part of this experiment
27
+ // the paste menu was completely redesigned, it was very difficult to ensure that an exposure event fires accurately
28
+ // for both control and test cohorts without executing code paths for both menus.
29
+ // This manual exposure event executes all criteria for showing AI buttons and fires the exposure manually in a code path that
30
+ // is guaranteed to execute on both control and test.
31
+ export var firePasteActionsMenuExperimentExposure = function firePasteActionsMenuExperimentExposure(contentLength, state, pasteStartPos, pasteEndPos, pastedText) {
32
+ if (contentLength < 100 || !pasteStartPos || !pasteEndPos || !pastedText) {
33
+ return;
34
+ }
35
+ if (isNotProse(pastedText)) {
36
+ return;
37
+ }
38
+ try {
39
+ var $pos = state.doc.resolve(pasteStartPos);
40
+ var pasteAncestorNodeNames = [];
41
+ for (var depth = $pos.depth; depth > 0; depth--) {
42
+ // Only include an ancestor if the entire pasted range is contained within it.
43
+ // This prevents nodes like 'heading' from being flagged as ancestors when the
44
+ // pasted content starts in a heading but extends beyond it (e.g. heading + paragraph).
45
+ if (pasteEndPos <= $pos.end(depth)) {
46
+ pasteAncestorNodeNames.push($pos.node(depth).type.name);
47
+ }
48
+ }
49
+ var isInExcludedNode = pasteAncestorNodeNames.some(function (name) {
50
+ return ['codeBlock', 'heading'].includes(name);
51
+ });
52
+ if (!isInExcludedNode) {
53
+ expVal('platform_editor_paste_actions_menu', 'isEnabled', false);
54
+ }
55
+ } catch (_unused) {
56
+ // pasteStartPos may be out of bounds if the document changed between
57
+ // when the paste was recorded and when this effect fires.
58
+ return;
59
+ }
60
+ };
@@ -0,0 +1,2 @@
1
+ import type { EditorState } from '@atlaskit/editor-prosemirror/state';
2
+ export declare const firePasteActionsMenuExperimentExposure: (contentLength: number, state: EditorState, pasteStartPos?: number, pasteEndPos?: number, pastedText?: string) => void;
@@ -0,0 +1,2 @@
1
+ import type { EditorState } from '@atlaskit/editor-prosemirror/state';
2
+ export declare const firePasteActionsMenuExperimentExposure: (contentLength: number, state: EditorState, pasteStartPos?: number, pasteEndPos?: number, pastedText?: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-paste-options-toolbar",
3
- "version": "9.1.7",
3
+ "version": "9.1.9",
4
4
  "description": "Paste options toolbar for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -33,7 +33,7 @@
33
33
  "@atlaskit/dropdown-menu": "^16.8.0",
34
34
  "@atlaskit/editor-markdown-transformer": "^5.20.0",
35
35
  "@atlaskit/editor-plugin-analytics": "^8.0.0",
36
- "@atlaskit/editor-plugin-paste": "^9.0.0",
36
+ "@atlaskit/editor-plugin-paste": "^9.1.0",
37
37
  "@atlaskit/editor-plugin-ui-control-registry": "^2.0.0",
38
38
  "@atlaskit/editor-prosemirror": "^7.3.0",
39
39
  "@atlaskit/editor-shared-styles": "^3.10.0",
@@ -42,14 +42,15 @@
42
42
  "@atlaskit/icon": "^33.1.0",
43
43
  "@atlaskit/platform-feature-flags": "^1.1.0",
44
44
  "@atlaskit/primitives": "^18.1.0",
45
- "@atlaskit/tokens": "^11.3.0",
45
+ "@atlaskit/tmp-editor-statsig": "^50.0.0",
46
+ "@atlaskit/tokens": "^11.4.0",
46
47
  "@babel/runtime": "^7.0.0",
47
48
  "@compiled/react": "^0.20.0",
48
49
  "@emotion/react": "^11.7.1",
49
50
  "react-intl-next": "npm:react-intl@^5.18.1"
50
51
  },
51
52
  "peerDependencies": {
52
- "@atlaskit/editor-common": "^112.11.0",
53
+ "@atlaskit/editor-common": "^112.13.0",
53
54
  "react": "^18.2.0",
54
55
  "react-dom": "^18.2.0"
55
56
  },
@@ -60,6 +61,9 @@
60
61
  "platform-feature-flags": {
61
62
  "platform_editor_paste_actions_keypress_fix": {
62
63
  "type": "boolean"
64
+ },
65
+ "platform_editor_paste_actions_menu_exposure": {
66
+ "type": "boolean"
63
67
  }
64
68
  },
65
69
  "techstack": {