@atlaskit/editor-plugin-synced-block 10.0.20 → 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,32 @@
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
+
20
+ ## 10.0.21
21
+
22
+ ### Patch Changes
23
+
24
+ - [`845de90a5f39e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/845de90a5f39e) -
25
+ [ux] Fix source synced block deletion intermittently failing when the document changes (from your
26
+ own edits or a collaborator's) while the delete confirmation dialog is open. The delete is now
27
+ recomputed from the current document on confirm, instead of replaying a stale transaction captured
28
+ when the dialog opened. Gated behind `platform_editor_blocks_patch_4`.
29
+
3
30
  ## 10.0.20
4
31
 
5
32
  ### 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,
@@ -171,9 +232,18 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
171
232
  });
172
233
  });
173
234
  if (bodiedSyncBlockRemoved.length > 0) {
174
- // eslint-disable-next-line no-param-reassign
175
- confirmationTransactionRef.current = tr;
176
- return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
235
+ if (!(0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
236
+ // Legacy stash-and-rebase path: store the original delete transaction so it
237
+ // can be rebased against intervening edits and replayed on confirm. When
238
+ // platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
239
+ // from the live document on confirm (see handleBodiedSyncBlockRemoval), so
240
+ // there is no need to stash the transaction here. See EDITOR-7889.
241
+ // eslint-disable-next-line no-param-reassign
242
+ confirmationTransactionRef.current = tr;
243
+ }
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);
177
247
  }
178
248
  if (bodiedSyncBlockAdded.length > 0) {
179
249
  if (tr.getMeta(_utils.pmHistoryPluginKey)) {
@@ -887,13 +957,19 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
887
957
  }
888
958
  });
889
959
  }
890
- trs.filter(function (tr) {
891
- return tr.docChanged;
892
- }).forEach(function (tr) {
893
- if (confirmationTransactionRef.current) {
894
- confirmationTransactionRef.current = (0, _editorSyncedBlockProvider.rebaseTransaction)(confirmationTransactionRef.current, tr, newState);
895
- }
896
- });
960
+
961
+ // Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
962
+ // delete is recomputed fresh from the live document on confirm, so there is
963
+ // no stashed transaction to rebase here. See EDITOR-7889.
964
+ if (!(0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
965
+ trs.filter(function (tr) {
966
+ return tr.docChanged;
967
+ }).forEach(function (tr) {
968
+ if (confirmationTransactionRef.current) {
969
+ confirmationTransactionRef.current = (0, _editorSyncedBlockProvider.rebaseTransaction)(confirmationTransactionRef.current, tr, newState);
970
+ }
971
+ });
972
+ }
897
973
  var _iterator2 = _createForOfIteratorHelper(trs),
898
974
  _step2;
899
975
  try {
@@ -7,8 +7,10 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.handleBodiedSyncBlockRemoval = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _utils = require("@atlaskit/editor-common/utils");
10
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
10
11
  var _types = require("../../types");
11
12
  var _main = require("../main");
13
+ var _recomputeDeleteTransaction = require("./recompute-delete-transaction");
12
14
  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; }
13
15
  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) { (0, _defineProperty2.default)(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
16
  var onRetry = function onRetry(api, syncBlockStore) {
@@ -32,7 +34,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
32
34
  });
33
35
  };
34
36
  };
35
- var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) {
37
+ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
36
38
  // Clear potential old pending deletion to retreat the deletion as first attempt
37
39
  syncBlockStore.sourceManager.clearPendingDeletion();
38
40
 
@@ -43,12 +45,45 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
43
45
  syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(function (node) {
44
46
  return node.attrs;
45
47
  }), deletionReason, function () {
46
- var _api$core2;
48
+ var _api$core3;
49
+ if ((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
50
+ var _api$core2;
51
+ // Recompute the delete fresh from the live document instead of
52
+ // replaying a stashed-and-rebased transaction. The stash/rebase
53
+ // approach produced stale, schema-invalid transactions when the
54
+ // document changed shape (local edits or remote collab) while the
55
+ // confirmation modal was open — the dominant signature being
56
+ // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
57
+ api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
58
+ var tr = _ref2.tr;
59
+ var recomputedTr = (0, _recomputeDeleteTransaction.recomputeDeleteTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
60
+ return node.attrs;
61
+ }));
62
+
63
+ // The target node(s) no longer exist in the live document (e.g.
64
+ // a remote collaborator already removed them). There is nothing
65
+ // to delete locally, so return `null` to skip the dispatch
66
+ // entirely — dispatching the untouched transaction would be a
67
+ // no-op that still runs the plugin's filter/append hooks. The
68
+ // backend deletion has already been issued by the store manager.
69
+ if (!recomputedTr) {
70
+ return null;
71
+ }
72
+ recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
73
+ if (!recomputedTr.getMeta(_utils.pmHistoryPluginKey)) {
74
+ // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
75
+ // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
76
+ recomputedTr.setMeta('addToHistory', false);
77
+ }
78
+ return recomputedTr;
79
+ });
80
+ return;
81
+ }
47
82
  var confirmationTransaction = confirmationTransactionRef.current;
48
83
  if (!confirmationTransaction) {
49
84
  return;
50
85
  }
51
- api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function () {
86
+ api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function () {
52
87
  var trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
53
88
  if (!trToDispatch.getMeta(_utils.pmHistoryPluginKey)) {
54
89
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
@@ -58,9 +93,9 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
58
93
  return trToDispatch;
59
94
  });
60
95
  }, function (success) {
61
- var _api$core3;
62
- api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function (_ref2) {
63
- var tr = _ref2.tr;
96
+ var _api$core4;
97
+ api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
98
+ var tr = _ref3.tr;
64
99
  var newState;
65
100
  if (!success) {
66
101
  newState = {
@@ -86,6 +121,6 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
86
121
  });
87
122
  }, function () {
88
123
  confirmationTransactionRef.current = undefined;
89
- });
124
+ }, mechanism);
90
125
  return false;
91
126
  };
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.recomputeDeleteTransaction = void 0;
7
+ /**
8
+ * Whether `node` is the source block requested by `wanted`.
9
+ *
10
+ * Matching is done *per requested block* (rather than against global pools of
11
+ * every requested localId/resourceId) so a node cannot be matched by combining
12
+ * one block's `localId` with a different block's `resourceId`. We match on the
13
+ * requested block's `localId` first and only fall back to its `resourceId` when
14
+ * the `localId` has been regenerated (so it no longer matches the live node).
15
+ */
16
+ var matchesRequestedBlock = function matchesRequestedBlock(node, wanted) {
17
+ var _ref = node.attrs,
18
+ localId = _ref.localId,
19
+ resourceId = _ref.resourceId;
20
+ if (wanted.localId && localId && wanted.localId === localId) {
21
+ return true;
22
+ }
23
+ return Boolean(wanted.resourceId && resourceId && wanted.resourceId === resourceId);
24
+ };
25
+
26
+ /**
27
+ * Locate the live positions of the source `bodiedSyncBlock` nodes identified by
28
+ * `syncBlockIds`, matching first on `localId` and falling back to `resourceId`.
29
+ *
30
+ * This walks the *current* document (`tr.doc`) rather than relying on positions
31
+ * captured when the delete was first triggered, so it is robust to intervening
32
+ * local edits and remote collab changes that occur while the confirmation modal
33
+ * is open.
34
+ */
35
+ var findSourceBlocks = function findSourceBlocks(tr, isSourceBlock, syncBlockIds) {
36
+ var matches = [];
37
+ tr.doc.descendants(function (node, pos) {
38
+ if (!isSourceBlock(node)) {
39
+ // bodiedSyncBlock is always a top-level node, so there is no need to
40
+ // descend into other branches looking for one.
41
+ return false;
42
+ }
43
+ if (syncBlockIds.some(function (wanted) {
44
+ return matchesRequestedBlock(node, wanted);
45
+ })) {
46
+ matches.push({
47
+ node: node,
48
+ pos: pos
49
+ });
50
+ }
51
+
52
+ // Never recurse into a source block's body.
53
+ return false;
54
+ });
55
+ return matches;
56
+ };
57
+
58
+ /**
59
+ * Recompute a source `bodiedSyncBlock` delete from the live document.
60
+ *
61
+ * Instead of replaying a transaction that was stashed when the delete was first
62
+ * triggered (and then manually rebased against every intervening edit — the
63
+ * fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>"
64
+ * and assorted position/open-depth errors), this finds the target node(s) by
65
+ * `localId`/`resourceId` in the current state and issues a fresh
66
+ * `tr.delete(pos, pos + nodeSize)` for each.
67
+ *
68
+ * Deletes are applied in reverse document order so earlier deletes do not
69
+ * invalidate the positions of later ones.
70
+ *
71
+ * Partial matches are handled gracefully: when only some of the requested
72
+ * blocks are still present (e.g. a remote collaborator removed the rest while
73
+ * the confirmation modal was open), the found ones are deleted and the missing
74
+ * ones are skipped. This is intentional — the backend deletion for every
75
+ * requested block has already been issued by the store manager, so the local
76
+ * transaction only needs to remove whatever is still in the live document.
77
+ *
78
+ * @returns the mutated transaction when at least one target node was found and
79
+ * deleted (this may be a partial delete if some targets were already gone),
80
+ * otherwise `undefined` when none of the targets exist any more — e.g. a remote
81
+ * collaborator already removed them all — so there is nothing to delete.
82
+ */
83
+ var recomputeDeleteTransaction = exports.recomputeDeleteTransaction = function recomputeDeleteTransaction(tr, isSourceBlock, syncBlockIds) {
84
+ var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
85
+ if (matches.length === 0) {
86
+ return undefined;
87
+ }
88
+
89
+ // Delete in reverse document order so positions remain valid across deletes.
90
+ matches.sort(function (a, b) {
91
+ return b.pos - a.pos;
92
+ }).forEach(function (_ref2) {
93
+ var node = _ref2.node,
94
+ pos = _ref2.pos;
95
+ tr.delete(pos, pos + node.nodeSize);
96
+ });
97
+ return tr;
98
+ };
@@ -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,
@@ -147,9 +205,18 @@ const filterTransactionOnline = ({
147
205
  });
148
206
  });
149
207
  if (bodiedSyncBlockRemoved.length > 0) {
150
- // eslint-disable-next-line no-param-reassign
151
- confirmationTransactionRef.current = tr;
152
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
208
+ if (!fg('platform_editor_blocks_patch_4')) {
209
+ // Legacy stash-and-rebase path: store the original delete transaction so it
210
+ // can be rebased against intervening edits and replayed on confirm. When
211
+ // platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
212
+ // from the live document on confirm (see handleBodiedSyncBlockRemoval), so
213
+ // there is no need to stash the transaction here. See EDITOR-7889.
214
+ // eslint-disable-next-line no-param-reassign
215
+ confirmationTransactionRef.current = tr;
216
+ }
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);
153
220
  }
154
221
  if (bodiedSyncBlockAdded.length > 0) {
155
222
  if (tr.getMeta(pmHistoryPluginKey)) {
@@ -844,11 +911,17 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
844
911
  }
845
912
  });
846
913
  }
847
- trs.filter(tr => tr.docChanged).forEach(tr => {
848
- if (confirmationTransactionRef.current) {
849
- confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
850
- }
851
- });
914
+
915
+ // Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
916
+ // delete is recomputed fresh from the live document on confirm, so there is
917
+ // no stashed transaction to rebase here. See EDITOR-7889.
918
+ if (!fg('platform_editor_blocks_patch_4')) {
919
+ trs.filter(tr => tr.docChanged).forEach(tr => {
920
+ if (confirmationTransactionRef.current) {
921
+ confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
922
+ }
923
+ });
924
+ }
852
925
  for (const tr of trs) {
853
926
  if (tr.getMeta(pmHistoryPluginKey)) {
854
927
  const {
@@ -1,6 +1,8 @@
1
1
  import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
2
+ import { fg } from '@atlaskit/platform-feature-flags';
2
3
  import { FLAG_ID } from '../../types';
3
4
  import { syncedBlockPluginKey } from '../main';
5
+ import { recomputeDeleteTransaction } from './recompute-delete-transaction';
4
6
  const onRetry = (api, syncBlockStore) => () => {
5
7
  var _api$core;
6
8
  api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
@@ -19,7 +21,7 @@ const onDismissed = syncBlockStore => tr => {
19
21
  bodiedSyncBlockDeletionStatus: 'none'
20
22
  });
21
23
  };
22
- export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) => {
24
+ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) => {
23
25
  // Clear potential old pending deletion to retreat the deletion as first attempt
24
26
  syncBlockStore.sourceManager.clearPendingDeletion();
25
27
 
@@ -28,12 +30,44 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
28
30
  // See editor-common/src/sync-block/sync-block-store-manager.ts for how we handle user confirmation and
29
31
  // proceed with deletion.
30
32
  syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(node => node.attrs), deletionReason, () => {
31
- var _api$core2;
33
+ var _api$core3;
34
+ if (fg('platform_editor_blocks_patch_4')) {
35
+ var _api$core2;
36
+ // Recompute the delete fresh from the live document instead of
37
+ // replaying a stashed-and-rebased transaction. The stash/rebase
38
+ // approach produced stale, schema-invalid transactions when the
39
+ // document changed shape (local edits or remote collab) while the
40
+ // confirmation modal was open — the dominant signature being
41
+ // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
42
+ api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
43
+ tr
44
+ }) => {
45
+ const recomputedTr = recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs));
46
+
47
+ // The target node(s) no longer exist in the live document (e.g.
48
+ // a remote collaborator already removed them). There is nothing
49
+ // to delete locally, so return `null` to skip the dispatch
50
+ // entirely — dispatching the untouched transaction would be a
51
+ // no-op that still runs the plugin's filter/append hooks. The
52
+ // backend deletion has already been issued by the store manager.
53
+ if (!recomputedTr) {
54
+ return null;
55
+ }
56
+ recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
57
+ if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
58
+ // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
59
+ // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
60
+ recomputedTr.setMeta('addToHistory', false);
61
+ }
62
+ return recomputedTr;
63
+ });
64
+ return;
65
+ }
32
66
  const confirmationTransaction = confirmationTransactionRef.current;
33
67
  if (!confirmationTransaction) {
34
68
  return;
35
69
  }
36
- api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(() => {
70
+ api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(() => {
37
71
  const trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
38
72
  if (!trToDispatch.getMeta(pmHistoryPluginKey)) {
39
73
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
@@ -43,8 +77,8 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
43
77
  return trToDispatch;
44
78
  });
45
79
  }, success => {
46
- var _api$core3;
47
- api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(({
80
+ var _api$core4;
81
+ api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions.execute(({
48
82
  tr
49
83
  }) => {
50
84
  let newState;
@@ -73,6 +107,6 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
73
107
  });
74
108
  }, () => {
75
109
  confirmationTransactionRef.current = undefined;
76
- });
110
+ }, mechanism);
77
111
  return false;
78
112
  };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Whether `node` is the source block requested by `wanted`.
3
+ *
4
+ * Matching is done *per requested block* (rather than against global pools of
5
+ * every requested localId/resourceId) so a node cannot be matched by combining
6
+ * one block's `localId` with a different block's `resourceId`. We match on the
7
+ * requested block's `localId` first and only fall back to its `resourceId` when
8
+ * the `localId` has been regenerated (so it no longer matches the live node).
9
+ */
10
+ const matchesRequestedBlock = (node, wanted) => {
11
+ const {
12
+ localId,
13
+ resourceId
14
+ } = node.attrs;
15
+ if (wanted.localId && localId && wanted.localId === localId) {
16
+ return true;
17
+ }
18
+ return Boolean(wanted.resourceId && resourceId && wanted.resourceId === resourceId);
19
+ };
20
+
21
+ /**
22
+ * Locate the live positions of the source `bodiedSyncBlock` nodes identified by
23
+ * `syncBlockIds`, matching first on `localId` and falling back to `resourceId`.
24
+ *
25
+ * This walks the *current* document (`tr.doc`) rather than relying on positions
26
+ * captured when the delete was first triggered, so it is robust to intervening
27
+ * local edits and remote collab changes that occur while the confirmation modal
28
+ * is open.
29
+ */
30
+ const findSourceBlocks = (tr, isSourceBlock, syncBlockIds) => {
31
+ const matches = [];
32
+ tr.doc.descendants((node, pos) => {
33
+ if (!isSourceBlock(node)) {
34
+ // bodiedSyncBlock is always a top-level node, so there is no need to
35
+ // descend into other branches looking for one.
36
+ return false;
37
+ }
38
+ if (syncBlockIds.some(wanted => matchesRequestedBlock(node, wanted))) {
39
+ matches.push({
40
+ node,
41
+ pos
42
+ });
43
+ }
44
+
45
+ // Never recurse into a source block's body.
46
+ return false;
47
+ });
48
+ return matches;
49
+ };
50
+
51
+ /**
52
+ * Recompute a source `bodiedSyncBlock` delete from the live document.
53
+ *
54
+ * Instead of replaying a transaction that was stashed when the delete was first
55
+ * triggered (and then manually rebased against every intervening edit — the
56
+ * fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>"
57
+ * and assorted position/open-depth errors), this finds the target node(s) by
58
+ * `localId`/`resourceId` in the current state and issues a fresh
59
+ * `tr.delete(pos, pos + nodeSize)` for each.
60
+ *
61
+ * Deletes are applied in reverse document order so earlier deletes do not
62
+ * invalidate the positions of later ones.
63
+ *
64
+ * Partial matches are handled gracefully: when only some of the requested
65
+ * blocks are still present (e.g. a remote collaborator removed the rest while
66
+ * the confirmation modal was open), the found ones are deleted and the missing
67
+ * ones are skipped. This is intentional — the backend deletion for every
68
+ * requested block has already been issued by the store manager, so the local
69
+ * transaction only needs to remove whatever is still in the live document.
70
+ *
71
+ * @returns the mutated transaction when at least one target node was found and
72
+ * deleted (this may be a partial delete if some targets were already gone),
73
+ * otherwise `undefined` when none of the targets exist any more — e.g. a remote
74
+ * collaborator already removed them all — so there is nothing to delete.
75
+ */
76
+ export const recomputeDeleteTransaction = (tr, isSourceBlock, syncBlockIds) => {
77
+ const matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
78
+ if (matches.length === 0) {
79
+ return undefined;
80
+ }
81
+
82
+ // Delete in reverse document order so positions remain valid across deletes.
83
+ matches.sort((a, b) => b.pos - a.pos).forEach(({
84
+ node,
85
+ pos
86
+ }) => {
87
+ tr.delete(pos, pos + node.nodeSize);
88
+ });
89
+ return tr;
90
+ };
@@ -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,
@@ -164,9 +225,18 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
164
225
  });
165
226
  });
166
227
  if (bodiedSyncBlockRemoved.length > 0) {
167
- // eslint-disable-next-line no-param-reassign
168
- confirmationTransactionRef.current = tr;
169
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
228
+ if (!fg('platform_editor_blocks_patch_4')) {
229
+ // Legacy stash-and-rebase path: store the original delete transaction so it
230
+ // can be rebased against intervening edits and replayed on confirm. When
231
+ // platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
232
+ // from the live document on confirm (see handleBodiedSyncBlockRemoval), so
233
+ // there is no need to stash the transaction here. See EDITOR-7889.
234
+ // eslint-disable-next-line no-param-reassign
235
+ confirmationTransactionRef.current = tr;
236
+ }
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);
170
240
  }
171
241
  if (bodiedSyncBlockAdded.length > 0) {
172
242
  if (tr.getMeta(pmHistoryPluginKey)) {
@@ -880,13 +950,19 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
880
950
  }
881
951
  });
882
952
  }
883
- trs.filter(function (tr) {
884
- return tr.docChanged;
885
- }).forEach(function (tr) {
886
- if (confirmationTransactionRef.current) {
887
- confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
888
- }
889
- });
953
+
954
+ // Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
955
+ // delete is recomputed fresh from the live document on confirm, so there is
956
+ // no stashed transaction to rebase here. See EDITOR-7889.
957
+ if (!fg('platform_editor_blocks_patch_4')) {
958
+ trs.filter(function (tr) {
959
+ return tr.docChanged;
960
+ }).forEach(function (tr) {
961
+ if (confirmationTransactionRef.current) {
962
+ confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
963
+ }
964
+ });
965
+ }
890
966
  var _iterator2 = _createForOfIteratorHelper(trs),
891
967
  _step2;
892
968
  try {
@@ -2,8 +2,10 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  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; }
3
3
  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; }
4
4
  import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
5
+ import { fg } from '@atlaskit/platform-feature-flags';
5
6
  import { FLAG_ID } from '../../types';
6
7
  import { syncedBlockPluginKey } from '../main';
8
+ import { recomputeDeleteTransaction } from './recompute-delete-transaction';
7
9
  var onRetry = function onRetry(api, syncBlockStore) {
8
10
  return function () {
9
11
  var _api$core;
@@ -25,7 +27,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
25
27
  });
26
28
  };
27
29
  };
28
- export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) {
30
+ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
29
31
  // Clear potential old pending deletion to retreat the deletion as first attempt
30
32
  syncBlockStore.sourceManager.clearPendingDeletion();
31
33
 
@@ -36,12 +38,45 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
36
38
  syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(function (node) {
37
39
  return node.attrs;
38
40
  }), deletionReason, function () {
39
- var _api$core2;
41
+ var _api$core3;
42
+ if (fg('platform_editor_blocks_patch_4')) {
43
+ var _api$core2;
44
+ // Recompute the delete fresh from the live document instead of
45
+ // replaying a stashed-and-rebased transaction. The stash/rebase
46
+ // approach produced stale, schema-invalid transactions when the
47
+ // document changed shape (local edits or remote collab) while the
48
+ // confirmation modal was open — the dominant signature being
49
+ // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
50
+ api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
51
+ var tr = _ref2.tr;
52
+ var recomputedTr = recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
53
+ return node.attrs;
54
+ }));
55
+
56
+ // The target node(s) no longer exist in the live document (e.g.
57
+ // a remote collaborator already removed them). There is nothing
58
+ // to delete locally, so return `null` to skip the dispatch
59
+ // entirely — dispatching the untouched transaction would be a
60
+ // no-op that still runs the plugin's filter/append hooks. The
61
+ // backend deletion has already been issued by the store manager.
62
+ if (!recomputedTr) {
63
+ return null;
64
+ }
65
+ recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
66
+ if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
67
+ // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
68
+ // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
69
+ recomputedTr.setMeta('addToHistory', false);
70
+ }
71
+ return recomputedTr;
72
+ });
73
+ return;
74
+ }
40
75
  var confirmationTransaction = confirmationTransactionRef.current;
41
76
  if (!confirmationTransaction) {
42
77
  return;
43
78
  }
44
- api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function () {
79
+ api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function () {
45
80
  var trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
46
81
  if (!trToDispatch.getMeta(pmHistoryPluginKey)) {
47
82
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
@@ -51,9 +86,9 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
51
86
  return trToDispatch;
52
87
  });
53
88
  }, function (success) {
54
- var _api$core3;
55
- api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function (_ref2) {
56
- var tr = _ref2.tr;
89
+ var _api$core4;
90
+ api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
91
+ var tr = _ref3.tr;
57
92
  var newState;
58
93
  if (!success) {
59
94
  newState = {
@@ -79,6 +114,6 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
79
114
  });
80
115
  }, function () {
81
116
  confirmationTransactionRef.current = undefined;
82
- });
117
+ }, mechanism);
83
118
  return false;
84
119
  };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Whether `node` is the source block requested by `wanted`.
3
+ *
4
+ * Matching is done *per requested block* (rather than against global pools of
5
+ * every requested localId/resourceId) so a node cannot be matched by combining
6
+ * one block's `localId` with a different block's `resourceId`. We match on the
7
+ * requested block's `localId` first and only fall back to its `resourceId` when
8
+ * the `localId` has been regenerated (so it no longer matches the live node).
9
+ */
10
+ var matchesRequestedBlock = function matchesRequestedBlock(node, wanted) {
11
+ var _ref = node.attrs,
12
+ localId = _ref.localId,
13
+ resourceId = _ref.resourceId;
14
+ if (wanted.localId && localId && wanted.localId === localId) {
15
+ return true;
16
+ }
17
+ return Boolean(wanted.resourceId && resourceId && wanted.resourceId === resourceId);
18
+ };
19
+
20
+ /**
21
+ * Locate the live positions of the source `bodiedSyncBlock` nodes identified by
22
+ * `syncBlockIds`, matching first on `localId` and falling back to `resourceId`.
23
+ *
24
+ * This walks the *current* document (`tr.doc`) rather than relying on positions
25
+ * captured when the delete was first triggered, so it is robust to intervening
26
+ * local edits and remote collab changes that occur while the confirmation modal
27
+ * is open.
28
+ */
29
+ var findSourceBlocks = function findSourceBlocks(tr, isSourceBlock, syncBlockIds) {
30
+ var matches = [];
31
+ tr.doc.descendants(function (node, pos) {
32
+ if (!isSourceBlock(node)) {
33
+ // bodiedSyncBlock is always a top-level node, so there is no need to
34
+ // descend into other branches looking for one.
35
+ return false;
36
+ }
37
+ if (syncBlockIds.some(function (wanted) {
38
+ return matchesRequestedBlock(node, wanted);
39
+ })) {
40
+ matches.push({
41
+ node: node,
42
+ pos: pos
43
+ });
44
+ }
45
+
46
+ // Never recurse into a source block's body.
47
+ return false;
48
+ });
49
+ return matches;
50
+ };
51
+
52
+ /**
53
+ * Recompute a source `bodiedSyncBlock` delete from the live document.
54
+ *
55
+ * Instead of replaying a transaction that was stashed when the delete was first
56
+ * triggered (and then manually rebased against every intervening edit — the
57
+ * fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>"
58
+ * and assorted position/open-depth errors), this finds the target node(s) by
59
+ * `localId`/`resourceId` in the current state and issues a fresh
60
+ * `tr.delete(pos, pos + nodeSize)` for each.
61
+ *
62
+ * Deletes are applied in reverse document order so earlier deletes do not
63
+ * invalidate the positions of later ones.
64
+ *
65
+ * Partial matches are handled gracefully: when only some of the requested
66
+ * blocks are still present (e.g. a remote collaborator removed the rest while
67
+ * the confirmation modal was open), the found ones are deleted and the missing
68
+ * ones are skipped. This is intentional — the backend deletion for every
69
+ * requested block has already been issued by the store manager, so the local
70
+ * transaction only needs to remove whatever is still in the live document.
71
+ *
72
+ * @returns the mutated transaction when at least one target node was found and
73
+ * deleted (this may be a partial delete if some targets were already gone),
74
+ * otherwise `undefined` when none of the targets exist any more — e.g. a remote
75
+ * collaborator already removed them all — so there is nothing to delete.
76
+ */
77
+ export var recomputeDeleteTransaction = function recomputeDeleteTransaction(tr, isSourceBlock, syncBlockIds) {
78
+ var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
79
+ if (matches.length === 0) {
80
+ return undefined;
81
+ }
82
+
83
+ // Delete in reverse document order so positions remain valid across deletes.
84
+ matches.sort(function (a, b) {
85
+ return b.pos - a.pos;
86
+ }).forEach(function (_ref2) {
87
+ var node = _ref2.node,
88
+ pos = _ref2.pos;
89
+ tr.delete(pos, pos + node.nodeSize);
90
+ });
91
+ return tr;
92
+ };
@@ -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;
@@ -0,0 +1,29 @@
1
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
2
+ import type { Transaction } from '@atlaskit/editor-prosemirror/state';
3
+ import type { SyncBlockAttrs } from '../../types';
4
+ /**
5
+ * Recompute a source `bodiedSyncBlock` delete from the live document.
6
+ *
7
+ * Instead of replaying a transaction that was stashed when the delete was first
8
+ * triggered (and then manually rebased against every intervening edit — the
9
+ * fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>"
10
+ * and assorted position/open-depth errors), this finds the target node(s) by
11
+ * `localId`/`resourceId` in the current state and issues a fresh
12
+ * `tr.delete(pos, pos + nodeSize)` for each.
13
+ *
14
+ * Deletes are applied in reverse document order so earlier deletes do not
15
+ * invalidate the positions of later ones.
16
+ *
17
+ * Partial matches are handled gracefully: when only some of the requested
18
+ * blocks are still present (e.g. a remote collaborator removed the rest while
19
+ * the confirmation modal was open), the found ones are deleted and the missing
20
+ * ones are skipped. This is intentional — the backend deletion for every
21
+ * requested block has already been issued by the store manager, so the local
22
+ * transaction only needs to remove whatever is still in the live document.
23
+ *
24
+ * @returns the mutated transaction when at least one target node was found and
25
+ * deleted (this may be a partial delete if some targets were already gone),
26
+ * otherwise `undefined` when none of the targets exist any more — e.g. a remote
27
+ * collaborator already removed them all — so there is nothing to delete.
28
+ */
29
+ export declare const recomputeDeleteTransaction: (tr: Transaction, isSourceBlock: (node: PMNode) => boolean, syncBlockIds: SyncBlockAttrs[]) => Transaction | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "10.0.20",
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
  },
@@ -108,6 +108,9 @@
108
108
  "platform_editor_blocks_patch_2": {
109
109
  "type": "boolean"
110
110
  },
111
+ "platform_editor_blocks_patch_4": {
112
+ "type": "boolean"
113
+ },
111
114
  "platform_synced_block_patch_13": {
112
115
  "type": "boolean"
113
116
  },