@atlaskit/editor-plugin-synced-block 10.0.21 → 10.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 10.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`ecafba7c81b7d`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ecafba7c81b7d) -
8
+ Enrich synced block deletion analytics so source-block deletions can be explained rather than just
9
+ counted. The source `syncedBlockDelete` operational event now carries `deletionReason` and a
10
+ `mechanism` dimension (undo/redo/deleteButton/keyboardDelete/selectionReplaced/other), and
11
+ repeated emissions for the same removal are de-duplicated. A real operational `syncedBlockCreate`
12
+ success event is now emitted, and the source bare-uuid join key is added to the copy event so
13
+ create/copy/delete can be correlated. All new attributes and events are gated behind
14
+ `platform_editor_blocks_patch_4`; gate-off behaviour is unchanged.
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies
19
+
3
20
  ## 10.0.21
4
21
 
5
22
  ### Patch Changes
@@ -158,6 +158,10 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
158
158
  }
159
159
  var domNode = (0, _copyButton.toDOM)(referenceSyncBlockNode, schema);
160
160
  (0, _copyButton.copyDomNode)(domNode, referenceSyncBlockNode.type, selection);
161
+
162
+ // Bare-uuid join key shared with the create/delete events: for a source
163
+ // bodiedSyncBlock its `localId` is the source uuid (copy was page-form only).
164
+ var sourceJoinKey = isBodiedSyncBlock ? syncBlockFindResult.node.attrs.localId : undefined;
161
165
  (0, _utils2.deferDispatch)(function () {
162
166
  api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
163
167
  var _api$analytics4;
@@ -167,10 +171,12 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
167
171
  action: _analytics.ACTION.COPIED,
168
172
  actionSubject: _analytics.ACTION_SUBJECT.SYNCED_BLOCK,
169
173
  actionSubjectId: _analytics.ACTION_SUBJECT_ID.SYNCED_BLOCK_COPY,
170
- attributes: {
174
+ attributes: _objectSpread({
171
175
  resourceId: referenceSyncBlockNode.attrs.resourceId,
172
176
  inputMethod: inputMethod
173
- }
177
+ }, sourceJoinKey && (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4') && {
178
+ blockInstanceId: sourceJoinKey
179
+ })
174
180
  });
175
181
  return tr.setMeta(_main.syncedBlockPluginKey, {
176
182
  activeFlag: {
@@ -246,6 +252,13 @@ var removeSyncedBlock = exports.removeSyncedBlock = function removeSyncedBlock(a
246
252
  if (!removeTr) {
247
253
  return false;
248
254
  }
255
+
256
+ // Tag the transaction so analytics can report this as `deleteButton` rather
257
+ // than a keyboard delete (both produce a plain ReplaceStep). Gated so the
258
+ // gate-off transaction is unchanged.
259
+ if ((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
260
+ removeTr.setMeta(_main.deleteMechanismMetaKey, 'deleteButton');
261
+ }
249
262
  dispatch(removeTr);
250
263
  api === null || api === void 0 || api.core.actions.focus();
251
264
  return true;
@@ -4,10 +4,11 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.syncedBlockPluginKey = exports.createPlugin = void 0;
7
+ exports.syncedBlockPluginKey = exports.getDeleteMechanism = exports.deleteMechanismMetaKey = exports.createPlugin = void 0;
8
8
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
+ var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
11
12
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
12
13
  var _analytics = require("@atlaskit/editor-common/analytics");
13
14
  var _collab = require("@atlaskit/editor-common/collab");
@@ -41,6 +42,17 @@ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol
41
42
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
42
43
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
43
44
  var syncedBlockPluginKey = exports.syncedBlockPluginKey = new _state.PluginKey('syncedBlockPlugin');
45
+
46
+ /**
47
+ * Dedicated transaction-meta {@link PluginKey} the synced-block removal command
48
+ * sets so {@link getDeleteMechanism} can report a toolbar Delete as
49
+ * `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
50
+ * plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
51
+ * carries plugin-state transitions read in `apply()`, or a bare string) keeps
52
+ * this transient signal uniquely namespaced and isolated. Only read behind
53
+ * `platform_editor_blocks_patch_4`.
54
+ */
55
+ var deleteMechanismMetaKey = exports.deleteMechanismMetaKey = new _state.PluginKey('syncedBlockDeleteMechanism');
44
56
  var mapRetryCreationPosMap = function mapRetryCreationPosMap(oldMap, newRetryCreationPos, mapPos, isPerfExperimentOn) {
45
57
  var resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;
46
58
 
@@ -121,6 +133,55 @@ var getDeleteReason = function getDeleteReason(tr) {
121
133
  }
122
134
  return reason;
123
135
  };
136
+
137
+ /** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
138
+ var isHistoryMeta = function isHistoryMeta(meta) {
139
+ return (0, _typeof2.default)(meta) === 'object' && meta !== null && 'redo' in meta;
140
+ };
141
+
142
+ /**
143
+ * Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
144
+ * analytics dimension. Each value names the user action; most-specific wins:
145
+ * - `undo` / `redo` — history transaction.
146
+ * - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
147
+ * (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
148
+ * - `selectionReplaced` — ReplaceStep on a node-selected block (the
149
+ * accidental-overwrite path: block selected, then typed/pasted over).
150
+ * - `keyboardDelete` — ReplaceStep removal at a caret/range selection
151
+ * (Backspace/Delete key).
152
+ * - `other` — anything else (unsync, conversion, code-dispatched, or a
153
+ * structural `ReplaceAroundStep` such as wrap/lift/unwrap).
154
+ *
155
+ * Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
156
+ * is used for wrapping/lifting/unwrapping content, not direct removal, so a
157
+ * removal carried by one is classified as `other` rather than misreported as a
158
+ * keyboard delete.
159
+ *
160
+ * `state` is the pre-transaction state, so its selection reflects what was
161
+ * selected when the edit was made.
162
+ */
163
+ var getDeleteMechanism = exports.getDeleteMechanism = function getDeleteMechanism(tr, state) {
164
+ var _selection$node;
165
+ var historyMeta = tr.getMeta(_utils.pmHistoryPluginKey);
166
+ if (historyMeta) {
167
+ return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
168
+ }
169
+
170
+ // The toolbar Delete control tags its transaction so we can report it
171
+ // distinctly from a keyboard delete (both produce a plain ReplaceStep).
172
+ if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
173
+ return 'deleteButton';
174
+ }
175
+ var hasReplaceStep = tr.steps.some(function (step) {
176
+ return step instanceof _transform.ReplaceStep;
177
+ });
178
+ if (!hasReplaceStep) {
179
+ return 'other';
180
+ }
181
+ var selection = state.selection;
182
+ var isNodeSelected = selection instanceof _state.NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
183
+ return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
184
+ };
124
185
  var filterTransactionOnline = function filterTransactionOnline(_ref3) {
125
186
  var _tr$getMeta;
126
187
  var tr = _ref3.tr,
@@ -180,7 +241,9 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
180
241
  // eslint-disable-next-line no-param-reassign
181
242
  confirmationTransactionRef.current = tr;
182
243
  }
183
- return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
244
+ // Only derive the mechanism behind the gate — discarded gate-off.
245
+ var mechanism = (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
246
+ return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
184
247
  }
185
248
  if (bodiedSyncBlockAdded.length > 0) {
186
249
  if (tr.getMeta(_utils.pmHistoryPluginKey)) {
@@ -34,7 +34,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
34
34
  });
35
35
  };
36
36
  };
37
- var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) {
37
+ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
38
38
  // Clear potential old pending deletion to retreat the deletion as first attempt
39
39
  syncBlockStore.sourceManager.clearPendingDeletion();
40
40
 
@@ -121,6 +121,6 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
121
121
  });
122
122
  }, function () {
123
123
  confirmationTransactionRef.current = undefined;
124
- });
124
+ }, mechanism);
125
125
  return false;
126
126
  };
@@ -5,7 +5,7 @@ import { DOMSerializer, Fragment } from '@atlaskit/editor-prosemirror/model';
5
5
  import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
6
6
  import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
7
7
  import { fg } from '@atlaskit/platform-feature-flags';
8
- import { syncedBlockPluginKey } from '../pm-plugins/main';
8
+ import { deleteMechanismMetaKey, syncedBlockPluginKey } from '../pm-plugins/main';
9
9
  import { canBeConvertedToSyncBlock, deferDispatch, findSyncBlock, findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
10
10
  import { FLAG_ID } from '../types';
11
11
  import { findBodiedSyncBlockByLocalId, pasteSyncBlockHTMLContent } from './utils';
@@ -153,6 +153,10 @@ const copySyncedBlockReferenceToClipboardInternal = (schema, selection, syncBloc
153
153
  }
154
154
  const domNode = toDOM(referenceSyncBlockNode, schema);
155
155
  copyDomNode(domNode, referenceSyncBlockNode.type, selection);
156
+
157
+ // Bare-uuid join key shared with the create/delete events: for a source
158
+ // bodiedSyncBlock its `localId` is the source uuid (copy was page-form only).
159
+ const sourceJoinKey = isBodiedSyncBlock ? syncBlockFindResult.node.attrs.localId : undefined;
156
160
  deferDispatch(() => {
157
161
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
158
162
  tr
@@ -165,7 +169,10 @@ const copySyncedBlockReferenceToClipboardInternal = (schema, selection, syncBloc
165
169
  actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_COPY,
166
170
  attributes: {
167
171
  resourceId: referenceSyncBlockNode.attrs.resourceId,
168
- inputMethod
172
+ inputMethod,
173
+ ...(sourceJoinKey && fg('platform_editor_blocks_patch_4') && {
174
+ blockInstanceId: sourceJoinKey
175
+ })
169
176
  }
170
177
  });
171
178
  return tr.setMeta(syncedBlockPluginKey, {
@@ -246,6 +253,13 @@ export const removeSyncedBlock = api => (state, dispatch, _view) => {
246
253
  if (!removeTr) {
247
254
  return false;
248
255
  }
256
+
257
+ // Tag the transaction so analytics can report this as `deleteButton` rather
258
+ // than a keyboard delete (both produce a plain ReplaceStep). Gated so the
259
+ // gate-off transaction is unchanged.
260
+ if (fg('platform_editor_blocks_patch_4')) {
261
+ removeTr.setMeta(deleteMechanismMetaKey, 'deleteButton');
262
+ }
249
263
  dispatch(removeTr);
250
264
  api === null || api === void 0 ? void 0 : api.core.actions.focus();
251
265
  return true;
@@ -6,7 +6,7 @@ import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
6
6
  import { BodiedSyncBlockSharedCssClassName, SyncBlockStateCssClassName } from '@atlaskit/editor-common/sync-block';
7
7
  import { mapSlice, pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
8
8
  import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
9
- import { PluginKey } from '@atlaskit/editor-prosemirror/state';
9
+ import { NodeSelection, PluginKey } from '@atlaskit/editor-prosemirror/state';
10
10
  import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
11
11
  import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
12
12
  import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
@@ -26,6 +26,17 @@ import { hasEditInSyncBlock, trackSyncBlocks } from './utils/track-sync-blocks';
26
26
  import { transactionInsertsSyncedBlock } from './utils/transaction-inserts-synced-block';
27
27
  import { deferDispatch, wasExtensionInsertedInBodiedSyncBlock, sliceFullyContainsNode } from './utils/utils';
28
28
  export const syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
29
+
30
+ /**
31
+ * Dedicated transaction-meta {@link PluginKey} the synced-block removal command
32
+ * sets so {@link getDeleteMechanism} can report a toolbar Delete as
33
+ * `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
34
+ * plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
35
+ * carries plugin-state transitions read in `apply()`, or a bare string) keeps
36
+ * this transient signal uniquely namespaced and isolated. Only read behind
37
+ * `platform_editor_blocks_patch_4`.
38
+ */
39
+ export const deleteMechanismMetaKey = new PluginKey('syncedBlockDeleteMechanism');
29
40
  const mapRetryCreationPosMap = (oldMap, newRetryCreationPos, mapPos, isPerfExperimentOn) => {
30
41
  const resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;
31
42
 
@@ -95,6 +106,53 @@ const getDeleteReason = tr => {
95
106
  }
96
107
  return reason;
97
108
  };
109
+
110
+ /** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
111
+ const isHistoryMeta = meta => typeof meta === 'object' && meta !== null && 'redo' in meta;
112
+
113
+ /**
114
+ * Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
115
+ * analytics dimension. Each value names the user action; most-specific wins:
116
+ * - `undo` / `redo` — history transaction.
117
+ * - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
118
+ * (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
119
+ * - `selectionReplaced` — ReplaceStep on a node-selected block (the
120
+ * accidental-overwrite path: block selected, then typed/pasted over).
121
+ * - `keyboardDelete` — ReplaceStep removal at a caret/range selection
122
+ * (Backspace/Delete key).
123
+ * - `other` — anything else (unsync, conversion, code-dispatched, or a
124
+ * structural `ReplaceAroundStep` such as wrap/lift/unwrap).
125
+ *
126
+ * Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
127
+ * is used for wrapping/lifting/unwrapping content, not direct removal, so a
128
+ * removal carried by one is classified as `other` rather than misreported as a
129
+ * keyboard delete.
130
+ *
131
+ * `state` is the pre-transaction state, so its selection reflects what was
132
+ * selected when the edit was made.
133
+ */
134
+ export const getDeleteMechanism = (tr, state) => {
135
+ var _selection$node;
136
+ const historyMeta = tr.getMeta(pmHistoryPluginKey);
137
+ if (historyMeta) {
138
+ return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
139
+ }
140
+
141
+ // The toolbar Delete control tags its transaction so we can report it
142
+ // distinctly from a keyboard delete (both produce a plain ReplaceStep).
143
+ if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
144
+ return 'deleteButton';
145
+ }
146
+ const hasReplaceStep = tr.steps.some(step => step instanceof ReplaceStep);
147
+ if (!hasReplaceStep) {
148
+ return 'other';
149
+ }
150
+ const {
151
+ selection
152
+ } = state;
153
+ const isNodeSelected = selection instanceof NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
154
+ return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
155
+ };
98
156
  const filterTransactionOnline = ({
99
157
  tr,
100
158
  state,
@@ -156,7 +214,9 @@ const filterTransactionOnline = ({
156
214
  // eslint-disable-next-line no-param-reassign
157
215
  confirmationTransactionRef.current = tr;
158
216
  }
159
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
217
+ // Only derive the mechanism behind the gate — discarded gate-off.
218
+ const mechanism = fg('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
219
+ return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
160
220
  }
161
221
  if (bodiedSyncBlockAdded.length > 0) {
162
222
  if (tr.getMeta(pmHistoryPluginKey)) {
@@ -21,7 +21,7 @@ const onDismissed = syncBlockStore => tr => {
21
21
  bodiedSyncBlockDeletionStatus: 'none'
22
22
  });
23
23
  };
24
- export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) => {
24
+ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) => {
25
25
  // Clear potential old pending deletion to retreat the deletion as first attempt
26
26
  syncBlockStore.sourceManager.clearPendingDeletion();
27
27
 
@@ -107,6 +107,6 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
107
107
  });
108
108
  }, () => {
109
109
  confirmationTransactionRef.current = undefined;
110
- });
110
+ }, mechanism);
111
111
  return false;
112
112
  };
@@ -8,7 +8,7 @@ import { DOMSerializer, Fragment } from '@atlaskit/editor-prosemirror/model';
8
8
  import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
9
9
  import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
10
10
  import { fg } from '@atlaskit/platform-feature-flags';
11
- import { syncedBlockPluginKey } from '../pm-plugins/main';
11
+ import { deleteMechanismMetaKey, syncedBlockPluginKey } from '../pm-plugins/main';
12
12
  import { canBeConvertedToSyncBlock, deferDispatch, findSyncBlock, findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
13
13
  import { FLAG_ID } from '../types';
14
14
  import { findBodiedSyncBlockByLocalId, pasteSyncBlockHTMLContent } from './utils';
@@ -151,6 +151,10 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
151
151
  }
152
152
  var domNode = toDOM(referenceSyncBlockNode, schema);
153
153
  copyDomNode(domNode, referenceSyncBlockNode.type, selection);
154
+
155
+ // Bare-uuid join key shared with the create/delete events: for a source
156
+ // bodiedSyncBlock its `localId` is the source uuid (copy was page-form only).
157
+ var sourceJoinKey = isBodiedSyncBlock ? syncBlockFindResult.node.attrs.localId : undefined;
154
158
  deferDispatch(function () {
155
159
  api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
156
160
  var _api$analytics4;
@@ -160,10 +164,12 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
160
164
  action: ACTION.COPIED,
161
165
  actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
162
166
  actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_COPY,
163
- attributes: {
167
+ attributes: _objectSpread({
164
168
  resourceId: referenceSyncBlockNode.attrs.resourceId,
165
169
  inputMethod: inputMethod
166
- }
170
+ }, sourceJoinKey && fg('platform_editor_blocks_patch_4') && {
171
+ blockInstanceId: sourceJoinKey
172
+ })
167
173
  });
168
174
  return tr.setMeta(syncedBlockPluginKey, {
169
175
  activeFlag: {
@@ -239,6 +245,13 @@ export var removeSyncedBlock = function removeSyncedBlock(api) {
239
245
  if (!removeTr) {
240
246
  return false;
241
247
  }
248
+
249
+ // Tag the transaction so analytics can report this as `deleteButton` rather
250
+ // than a keyboard delete (both produce a plain ReplaceStep). Gated so the
251
+ // gate-off transaction is unchanged.
252
+ if (fg('platform_editor_blocks_patch_4')) {
253
+ removeTr.setMeta(deleteMechanismMetaKey, 'deleteButton');
254
+ }
242
255
  dispatch(removeTr);
243
256
  api === null || api === void 0 || api.core.actions.focus();
244
257
  return true;
@@ -1,6 +1,7 @@
1
1
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
2
  import _createClass from "@babel/runtime/helpers/createClass";
3
3
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ import _typeof from "@babel/runtime/helpers/typeof";
4
5
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
5
6
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
6
7
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -14,7 +15,7 @@ import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
14
15
  import { BodiedSyncBlockSharedCssClassName, SyncBlockStateCssClassName } from '@atlaskit/editor-common/sync-block';
15
16
  import { mapSlice, pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
16
17
  import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
17
- import { PluginKey } from '@atlaskit/editor-prosemirror/state';
18
+ import { NodeSelection, PluginKey } from '@atlaskit/editor-prosemirror/state';
18
19
  import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
19
20
  import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
20
21
  import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
@@ -34,6 +35,17 @@ import { hasEditInSyncBlock, trackSyncBlocks } from './utils/track-sync-blocks';
34
35
  import { transactionInsertsSyncedBlock } from './utils/transaction-inserts-synced-block';
35
36
  import { deferDispatch, wasExtensionInsertedInBodiedSyncBlock, sliceFullyContainsNode } from './utils/utils';
36
37
  export var syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
38
+
39
+ /**
40
+ * Dedicated transaction-meta {@link PluginKey} the synced-block removal command
41
+ * sets so {@link getDeleteMechanism} can report a toolbar Delete as
42
+ * `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
43
+ * plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
44
+ * carries plugin-state transitions read in `apply()`, or a bare string) keeps
45
+ * this transient signal uniquely namespaced and isolated. Only read behind
46
+ * `platform_editor_blocks_patch_4`.
47
+ */
48
+ export var deleteMechanismMetaKey = new PluginKey('syncedBlockDeleteMechanism');
37
49
  var mapRetryCreationPosMap = function mapRetryCreationPosMap(oldMap, newRetryCreationPos, mapPos, isPerfExperimentOn) {
38
50
  var resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;
39
51
 
@@ -114,6 +126,55 @@ var getDeleteReason = function getDeleteReason(tr) {
114
126
  }
115
127
  return reason;
116
128
  };
129
+
130
+ /** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
131
+ var isHistoryMeta = function isHistoryMeta(meta) {
132
+ return _typeof(meta) === 'object' && meta !== null && 'redo' in meta;
133
+ };
134
+
135
+ /**
136
+ * Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
137
+ * analytics dimension. Each value names the user action; most-specific wins:
138
+ * - `undo` / `redo` — history transaction.
139
+ * - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
140
+ * (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
141
+ * - `selectionReplaced` — ReplaceStep on a node-selected block (the
142
+ * accidental-overwrite path: block selected, then typed/pasted over).
143
+ * - `keyboardDelete` — ReplaceStep removal at a caret/range selection
144
+ * (Backspace/Delete key).
145
+ * - `other` — anything else (unsync, conversion, code-dispatched, or a
146
+ * structural `ReplaceAroundStep` such as wrap/lift/unwrap).
147
+ *
148
+ * Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
149
+ * is used for wrapping/lifting/unwrapping content, not direct removal, so a
150
+ * removal carried by one is classified as `other` rather than misreported as a
151
+ * keyboard delete.
152
+ *
153
+ * `state` is the pre-transaction state, so its selection reflects what was
154
+ * selected when the edit was made.
155
+ */
156
+ export var getDeleteMechanism = function getDeleteMechanism(tr, state) {
157
+ var _selection$node;
158
+ var historyMeta = tr.getMeta(pmHistoryPluginKey);
159
+ if (historyMeta) {
160
+ return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
161
+ }
162
+
163
+ // The toolbar Delete control tags its transaction so we can report it
164
+ // distinctly from a keyboard delete (both produce a plain ReplaceStep).
165
+ if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
166
+ return 'deleteButton';
167
+ }
168
+ var hasReplaceStep = tr.steps.some(function (step) {
169
+ return step instanceof ReplaceStep;
170
+ });
171
+ if (!hasReplaceStep) {
172
+ return 'other';
173
+ }
174
+ var selection = state.selection;
175
+ var isNodeSelected = selection instanceof NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
176
+ return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
177
+ };
117
178
  var filterTransactionOnline = function filterTransactionOnline(_ref3) {
118
179
  var _tr$getMeta;
119
180
  var tr = _ref3.tr,
@@ -173,7 +234,9 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
173
234
  // eslint-disable-next-line no-param-reassign
174
235
  confirmationTransactionRef.current = tr;
175
236
  }
176
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
237
+ // Only derive the mechanism behind the gate — discarded gate-off.
238
+ var mechanism = fg('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
239
+ return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
177
240
  }
178
241
  if (bodiedSyncBlockAdded.length > 0) {
179
242
  if (tr.getMeta(pmHistoryPluginKey)) {
@@ -27,7 +27,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
27
27
  });
28
28
  };
29
29
  };
30
- export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) {
30
+ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
31
31
  // Clear potential old pending deletion to retreat the deletion as first attempt
32
32
  syncBlockStore.sourceManager.clearPendingDeletion();
33
33
 
@@ -114,6 +114,6 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
114
114
  });
115
115
  }, function () {
116
116
  confirmationTransactionRef.current = undefined;
117
- });
117
+ }, mechanism);
118
118
  return false;
119
119
  };
@@ -1,11 +1,23 @@
1
1
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
2
  import type { ExtractInjectionAPI, PMPluginFactoryParams } from '@atlaskit/editor-common/types';
3
+ import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
3
4
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
4
5
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
5
6
  import type { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
7
+ import type { DeletionMechanism } from '@atlaskit/editor-synced-block-provider/common/types';
6
8
  import type { SyncedBlockPlugin, SyncedBlockPluginOptions } from '../syncedBlockPluginType';
7
9
  import type { ActiveFlag, BodiedSyncBlockDeletionStatus, RetryCreationPosMap } from '../types';
8
10
  export declare const syncedBlockPluginKey: PluginKey;
11
+ /**
12
+ * Dedicated transaction-meta {@link PluginKey} the synced-block removal command
13
+ * sets so {@link getDeleteMechanism} can report a toolbar Delete as
14
+ * `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
15
+ * plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
16
+ * carries plugin-state transitions read in `apply()`, or a bare string) keeps
17
+ * this transient signal uniquely namespaced and isolated. Only read behind
18
+ * `platform_editor_blocks_patch_4`.
19
+ */
20
+ export declare const deleteMechanismMetaKey: PluginKey<DeletionMechanism>;
9
21
  type SyncedBlockPluginState = {
10
22
  activeFlag: ActiveFlag;
11
23
  bodiedSyncBlockDeletionStatus?: BodiedSyncBlockDeletionStatus;
@@ -42,5 +54,27 @@ type SyncedBlockPluginState = {
42
54
  statusDecorationSet: DecorationSet;
43
55
  syncBlockStore: SyncBlockStoreManager;
44
56
  };
57
+ /**
58
+ * Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
59
+ * analytics dimension. Each value names the user action; most-specific wins:
60
+ * - `undo` / `redo` — history transaction.
61
+ * - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
62
+ * (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
63
+ * - `selectionReplaced` — ReplaceStep on a node-selected block (the
64
+ * accidental-overwrite path: block selected, then typed/pasted over).
65
+ * - `keyboardDelete` — ReplaceStep removal at a caret/range selection
66
+ * (Backspace/Delete key).
67
+ * - `other` — anything else (unsync, conversion, code-dispatched, or a
68
+ * structural `ReplaceAroundStep` such as wrap/lift/unwrap).
69
+ *
70
+ * Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
71
+ * is used for wrapping/lifting/unwrapping content, not direct removal, so a
72
+ * removal carried by one is classified as `other` rather than misreported as a
73
+ * keyboard delete.
74
+ *
75
+ * `state` is the pre-transaction state, so its selection reflects what was
76
+ * selected when the edit was made.
77
+ */
78
+ export declare const getDeleteMechanism: (tr: Transaction, state: EditorState) => DeletionMechanism;
45
79
  export declare const createPlugin: (options: SyncedBlockPluginOptions | undefined, pmPluginFactoryParams: PMPluginFactoryParams, syncBlockStore: SyncBlockStoreManager, api?: ExtractInjectionAPI<SyncedBlockPlugin>) => SafePlugin<SyncedBlockPluginState>;
46
80
  export {};
@@ -1,9 +1,10 @@
1
1
  import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
2
2
  import type { Transaction } from '@atlaskit/editor-prosemirror/state';
3
- import type { DeletionReason, SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
3
+ import type { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
4
+ import type { DeletionMechanism, DeletionReason } from '@atlaskit/editor-synced-block-provider/common/types';
4
5
  import type { SyncedBlockPlugin } from '../../syncedBlockPluginType';
5
6
  import type { SyncBlockInfo } from '../../types';
6
7
  export type TransactionRef = {
7
8
  current: Transaction | undefined;
8
9
  };
9
- export declare const handleBodiedSyncBlockRemoval: (bodiedSyncBlockRemoved: SyncBlockInfo[], syncBlockStore: SyncBlockStoreManager, api: ExtractInjectionAPI<SyncedBlockPlugin> | undefined, confirmationTransactionRef: TransactionRef, deletionReason: DeletionReason) => boolean;
10
+ export declare const handleBodiedSyncBlockRemoval: (bodiedSyncBlockRemoved: SyncBlockInfo[], syncBlockStore: SyncBlockStoreManager, api: ExtractInjectionAPI<SyncedBlockPlugin> | undefined, confirmationTransactionRef: TransactionRef, deletionReason: DeletionReason, mechanism?: DeletionMechanism) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "10.0.21",
3
+ "version": "10.1.0",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -35,7 +35,7 @@
35
35
  "@atlaskit/editor-plugin-user-intent": "^10.0.0",
36
36
  "@atlaskit/editor-prosemirror": "^8.0.0",
37
37
  "@atlaskit/editor-shared-styles": "^4.0.0",
38
- "@atlaskit/editor-synced-block-provider": "^8.4.0",
38
+ "@atlaskit/editor-synced-block-provider": "^8.5.0",
39
39
  "@atlaskit/editor-toolbar": "^2.1.0",
40
40
  "@atlaskit/flag": "^18.1.0",
41
41
  "@atlaskit/icon": "36.1.0",
@@ -56,7 +56,7 @@
56
56
  "date-fns": "^2.17.0"
57
57
  },
58
58
  "peerDependencies": {
59
- "@atlaskit/editor-common": "^116.18.0",
59
+ "@atlaskit/editor-common": "^116.19.0",
60
60
  "react": "^18.2.0",
61
61
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
62
62
  },