@atlaskit/editor-plugin-synced-block 15.0.10 → 16.0.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,34 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 16.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 15.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`db3dc405020c5`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/db3dc405020c5) -
14
+ Fix undo of a source synced block creation deleting the converted content (e.g. a table). Behind
15
+ the `platform_editor_blocks_exp_patch_10` experiment, once the deletion is confirmed and persisted
16
+ the undo is replayed against the live editor state instead of recomputing a whole-node delete, and
17
+ a redo of the creation unwraps the block instead of deleting its content.
18
+ `@atlaskit/editor-plugin-history` gains an `undo` `EditorCommand` that builds the next history
19
+ undo onto a given transaction. EDITOR-8863
20
+
21
+ ### Patch Changes
22
+
23
+ - [`abbc0c11100c8`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/abbc0c11100c8) -
24
+ EDITOR-8863: replace platform_editor_blocks_patch_10 feature gate with
25
+ platform_editor_blocks_exp_patch_10 experiment for block menu close on confirmed sync block
26
+ deletion
27
+ - [`b12eed2cd2633`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/b12eed2cd2633) -
28
+ Add node type analytics for non-empty synced block conversions behind the
29
+ platform_editor_sync_block_node_types experiment.
30
+ - Updated dependencies
31
+
3
32
  ## 15.0.10
4
33
 
5
34
  ### Patch Changes
@@ -64,6 +64,9 @@
64
64
  {
65
65
  "path": "../../editor-plugin-focus/afm-products/tsconfig.json"
66
66
  },
67
+ {
68
+ "path": "../../editor-plugin-history/afm-products/tsconfig.json"
69
+ },
67
70
  {
68
71
  "path": "../../editor-plugin-selection/afm-products/tsconfig.json"
69
72
  },
@@ -6,13 +6,16 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.unsync = exports.removeSyncedBlockAtPos = exports.removeSyncedBlock = exports.editSyncedBlockSource = exports.createSyncedBlock = exports.copySyncedBlockReferenceToClipboardEditorCommand = exports.copySyncedBlockReferenceToClipboard = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
9
10
  var _schemaDefault = require("@atlaskit/adf-schema/schema-default");
10
11
  var _analytics = require("@atlaskit/editor-common/analytics");
11
12
  var _copyButton = require("@atlaskit/editor-common/copy-button");
13
+ var _selection = require("@atlaskit/editor-common/selection");
12
14
  var _model = require("@atlaskit/editor-prosemirror/model");
13
15
  var _state = require("@atlaskit/editor-prosemirror/state");
14
16
  var _utils = require("@atlaskit/editor-prosemirror/utils");
15
17
  var _utils2 = require("@atlaskit/editor-synced-block-provider/utils");
18
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
16
19
  var _fg = require("@atlaskit/platform-feature-flags/fg");
17
20
  var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
18
21
  var _main = require("../pm-plugins/main");
@@ -90,6 +93,7 @@ var createSyncedBlock = exports.createSyncedBlock = function createSyncedBlock(_
90
93
  // set on the final transaction before returning (see below), since the
91
94
  // typeahead path may reassign `tr` and drop meta set here.
92
95
  var createdEmpty = tr.selection.empty;
96
+ var nodeTypes;
93
97
 
94
98
  // If the selection is empty, we want to insert the sync block on a new line
95
99
  if (tr.selection.empty) {
@@ -131,6 +135,11 @@ var createSyncedBlock = exports.createSyncedBlock = function createSyncedBlock(_
131
135
  });
132
136
  return false;
133
137
  }
138
+ if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_sync_block_node_types')) {
139
+ nodeTypes = (0, _toConsumableArray2.default)(new Set((0, _selection.getSourceNodesFromSelectionRange)(tr, tr.selection).map(function (node) {
140
+ return node.type.name;
141
+ }))).sort();
142
+ }
134
143
  var _attrs = syncBlockStore.sourceManager.generateBodiedSyncBlockAttrs();
135
144
  var _newBodiedSyncBlockNode = bodiedSyncBlock.createAndFill(_attrs, conversionInfo.contentToInclude, (0, _fg.fg)('platform_editor_blocks_patch_8') && conversionInfo.breakoutMark ? [conversionInfo.breakoutMark] : undefined);
136
145
  if (!_newBodiedSyncBlockNode) {
@@ -152,12 +161,12 @@ var createSyncedBlock = exports.createSyncedBlock = function createSyncedBlock(_
152
161
  tr = placeCaretInsideBodiedSyncBlock(tr, _newBodiedSyncBlockNode.attrs.localId);
153
162
  }
154
163
 
155
- // Stash creation-type signals on the final transaction (after any typeahead
156
- // reassignment). Set unconditionally — the store manager only reads it behind
157
- // the feature gate.
164
+ // Stash creation-type signals on the final transaction after any typeahead
165
+ // reassignment. nodeTypes is populated only for gated non-empty conversions.
158
166
  tr.setMeta(_main.creationMetaKey, {
159
167
  createdEmpty: createdEmpty,
160
- inputMethod: inputMethod
168
+ inputMethod: inputMethod,
169
+ nodeTypes: nodeTypes
161
170
  });
162
171
  return tr;
163
172
  };
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.syncedBlockPluginKey = exports.getPromptedFeedbackEntryPoint = exports.getDeleteMechanism = exports.deleteMechanismMetaKey = exports.creationMetaKey = exports.createPlugin = void 0;
8
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
8
9
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
10
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
11
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
@@ -17,11 +18,13 @@ var _selection = require("@atlaskit/editor-common/selection");
17
18
  var _syncBlock = require("@atlaskit/editor-common/sync-block");
18
19
  var _utils = require("@atlaskit/editor-common/utils");
19
20
  var _editorPluginConnectivity = require("@atlaskit/editor-plugin-connectivity");
21
+ var _model = require("@atlaskit/editor-prosemirror/model");
20
22
  var _state = require("@atlaskit/editor-prosemirror/state");
21
23
  var _transform = require("@atlaskit/editor-prosemirror/transform");
22
24
  var _view2 = require("@atlaskit/editor-prosemirror/view");
23
25
  var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
24
26
  var _utils2 = require("@atlaskit/editor-synced-block-provider/utils");
27
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
25
28
  var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
26
29
  var _bodiedSyncedBlock = require("../nodeviews/bodiedSyncedBlock");
27
30
  var _syncedBlock = require("../nodeviews/syncedBlock");
@@ -53,7 +56,7 @@ var deleteMechanismMetaKey = exports.deleteMechanismMetaKey = new _state.PluginK
53
56
  var syncedBlockPromptedFeedbackMetaKey = new _state.PluginKey('syncedBlockPromptedFeedback');
54
57
 
55
58
  /**
56
- * Creation-type signals set by {@link createSyncedBlock} on the creating
59
+ * Creation analytics signals set by {@link createSyncedBlock} on the creating
57
60
  * transaction. The async creation handler forwards them to the store manager,
58
61
  * which attaches them to the `syncedBlockCreate` event. A dedicated key keeps
59
62
  * this transient signal isolated from plugin state.
@@ -319,9 +322,10 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
319
322
  blockType: 'source',
320
323
  entryPoint: entryPoint
321
324
  } : undefined;
325
+ var feedbackCallbacks;
322
326
  if (feedbackContext) {
323
327
  var sourceAttempt = ctx.queueSourceFeedback(feedbackContext);
324
- return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, {
328
+ feedbackCallbacks = {
325
329
  onDeleteCompleted: function onDeleteCompleted(success) {
326
330
  return ctx.completeSourceDeletion(sourceAttempt, success);
327
331
  },
@@ -334,9 +338,17 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
334
338
  onDestroy: function onDestroy() {
335
339
  return ctx.clearSourceFeedback(sourceAttempt);
336
340
  }
337
- });
341
+ };
338
342
  }
339
- return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, undefined);
343
+ return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)({
344
+ removed: bodiedSyncBlockRemoved,
345
+ syncBlockStore: syncBlockStore,
346
+ api: api,
347
+ confirmationTransactionRef: confirmationTransactionRef,
348
+ deletionReason: getDeleteReason(tr),
349
+ mechanism: mechanism,
350
+ feedbackCallbacks: feedbackCallbacks
351
+ });
340
352
  }
341
353
  if (bodiedSyncBlockAdded.length > 0) {
342
354
  if (tr.getMeta(_utils.pmHistoryPluginKey)) {
@@ -356,7 +368,7 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
356
368
  return true;
357
369
  }
358
370
 
359
- // Forward creation-type signals captured by createSyncedBlock onto the async
371
+ // Forward creation analytics signals captured by createSyncedBlock onto the async
360
372
  // `syncedBlockCreate` success event.
361
373
  var creationMeta = tr.getMeta(creationMetaKey);
362
374
  (0, _handleBodiedSyncBlockCreation.handleBodiedSyncBlockCreation)(bodiedSyncBlockAdded, state, api, creationMeta);
@@ -1141,14 +1153,35 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
1141
1153
  var _trackSyncBlocks6 = (0, _trackSyncBlocks7.trackSyncBlocks)(syncBlockStore.sourceManager.isSourceBlock, tr, oldState),
1142
1154
  added = _trackSyncBlocks6.added;
1143
1155
  if (added.length > 0) {
1144
- // Delete bodiedSyncBlock if it's originated from history, i.e. redo creation
1145
- // See filterTransaction above for more details
1156
+ // A bodiedSyncBlock must not be (re)created from history, i.e. redo of a
1157
+ // creation (see filterTransaction above). Unwrap it instead of deleting it:
1158
+ // deleting the whole node also dropped the content the block was converted
1159
+ // from (e.g. a table), and undoing that delete re-added the block and hit
1160
+ // this guard again, so the content was unrecoverable (EDITOR-8863).
1146
1161
  var _tr = newState.tr;
1147
- added.forEach(function (node) {
1148
- if (node.from !== undefined && node.to !== undefined) {
1149
- _tr.delete(node.from, node.to);
1150
- }
1151
- });
1162
+ if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_blocks_exp_patch_10')) {
1163
+ // Unwrap in reverse document order so earlier replacements keep later
1164
+ // positions valid.
1165
+ (0, _toConsumableArray2.default)(added).sort(function (a, b) {
1166
+ var _b$from, _a$from;
1167
+ return ((_b$from = b.from) !== null && _b$from !== void 0 ? _b$from : 0) - ((_a$from = a.from) !== null && _a$from !== void 0 ? _a$from : 0);
1168
+ }).forEach(function (_ref12) {
1169
+ var node = _ref12.node,
1170
+ from = _ref12.from,
1171
+ to = _ref12.to;
1172
+ if (node && from !== undefined && to !== undefined) {
1173
+ // This is ProseMirror's Transaction.replace (a document edit), not String.replace.
1174
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
1175
+ _tr.replace(from, to, new _model.Slice(node.content, 0, 0));
1176
+ }
1177
+ });
1178
+ } else {
1179
+ added.forEach(function (node) {
1180
+ if (node.from !== undefined && node.to !== undefined) {
1181
+ _tr.delete(node.from, node.to);
1182
+ }
1183
+ });
1184
+ }
1152
1185
  return {
1153
1186
  v: _tr
1154
1187
  };
@@ -12,7 +12,7 @@ var _main = require("../main");
12
12
  var _utils = require("./utils");
13
13
  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; }
14
14
  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; }
15
- var onRetry = function onRetry(api, resourceId) {
15
+ var onRetry = function onRetry(api, resourceId, enrichment) {
16
16
  return function () {
17
17
  var _api$core, _api$core2;
18
18
  api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.focus();
@@ -28,7 +28,7 @@ var onRetry = function onRetry(api, resourceId) {
28
28
  tr.setSelection(_state.TextSelection.create(tr.doc, from, to)).setMeta(_main.syncedBlockPluginKey, {
29
29
  activeFlag: false
30
30
  });
31
- api === null || api === void 0 || (_api$syncedBlock2 = api.syncedBlock) === null || _api$syncedBlock2 === void 0 || _api$syncedBlock2.commands.insertSyncedBlock()({
31
+ api === null || api === void 0 || (_api$syncedBlock2 = api.syncedBlock) === null || _api$syncedBlock2 === void 0 || _api$syncedBlock2.commands.insertSyncedBlock(enrichment === null || enrichment === void 0 ? void 0 : enrichment.inputMethod)({
32
32
  tr: tr
33
33
  });
34
34
  return tr;
@@ -76,7 +76,7 @@ var buildRevertCreationTr = function buildRevertCreationTr(tr, pos) {
76
76
  *
77
77
  * Save the new bodiedSyncBlock to backend with empty content and handles revert (if failed) and retry flow.
78
78
  *
79
- * @param enrichment optional creation-type signals from `createSyncedBlock`,
79
+ * @param enrichment optional creation analytics signals from `createSyncedBlock`,
80
80
  * forwarded to the store manager to attach to the `syncedBlockCreate` event.
81
81
  */
82
82
  var handleBodiedSyncBlockCreation = exports.handleBodiedSyncBlockCreation = function handleBodiedSyncBlockCreation(bodiedSyncBlockAdded, editorState, api, enrichment) {
@@ -127,7 +127,7 @@ var handleBodiedSyncBlockCreation = exports.handleBodiedSyncBlockCreation = func
127
127
  return revertTr.setMeta('isConfirmedSyncBlockDeletion', true).setMeta('addToHistory', false).setMeta(_main.syncedBlockPluginKey, {
128
128
  activeFlag: {
129
129
  id: _types.FLAG_ID.CANNOT_CREATE_SYNC_BLOCK,
130
- onRetry: onRetry(api, resourceId),
130
+ onRetry: onRetry(api, resourceId, enrichment),
131
131
  onDismissed: function onDismissed(tr) {
132
132
  return tr.setMeta(_main.syncedBlockPluginKey, _objectSpread(_objectSpread({}, tr.getMeta(_main.syncedBlockPluginKey)), {}, {
133
133
  retryCreationPos: {
@@ -7,7 +7,7 @@ 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 _fg = require("@atlaskit/platform-feature-flags/fg");
10
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
11
11
  var _types = require("../../types");
12
12
  var _main = require("../main");
13
13
  var _recomputeDeleteTransaction = require("./recompute-delete-transaction");
@@ -34,7 +34,81 @@ var onDismissed = function onDismissed(syncBlockStore) {
34
34
  });
35
35
  };
36
36
  };
37
- var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism, feedbackCallbacks) {
37
+ var topLevelSourceBlocks = function topLevelSourceBlocks(doc, isSourceBlock) {
38
+ var blocks = [];
39
+ doc.forEach(function (node) {
40
+ if (isSourceBlock(node)) {
41
+ blocks.push(node);
42
+ }
43
+ });
44
+ return blocks;
45
+ };
46
+
47
+ /**
48
+ * Whether `tr` removes every block in `wanted` from the document. Identity follows
49
+ * `matchesRequestedBlock` (localId, falling back to resourceId) so this agrees with the
50
+ * recompute helpers.
51
+ */
52
+ var removesRequestedBlocks = function removesRequestedBlocks(tr, isSourceBlock, wanted) {
53
+ var before = topLevelSourceBlocks(tr.before, isSourceBlock);
54
+ var after = topLevelSourceBlocks(tr.doc, isSourceBlock);
55
+ return wanted.every(function (block) {
56
+ return before.some(function (node) {
57
+ return (0, _recomputeDeleteTransaction.matchesRequestedBlock)(node, block);
58
+ }) && !after.some(function (node) {
59
+ return (0, _recomputeDeleteTransaction.matchesRequestedBlock)(node, block);
60
+ });
61
+ });
62
+ };
63
+
64
+ /**
65
+ * Replay the undo that removed `wanted` once the source deletion has been confirmed and
66
+ * persisted (EDITOR-8863).
67
+ *
68
+ * The original undo transaction was rejected by `filterTransaction` so the confirmation could
69
+ * run, which leaves the history event on top of the undo stack. Building the undo again from
70
+ * the live state (via the history plugin) restores whatever the block was converted from
71
+ * (e.g. a table), unlike a recomputed whole-node delete which drops that content.
72
+ *
73
+ * Guard: only dispatch if that undo still removes the same source block(s). If the user
74
+ * edited in the meantime the top of the stack is no longer that event, so return `false` and
75
+ * let the caller fall back to the recomputed unwrap.
76
+ */
77
+ var replayConfirmedUndo = function replayConfirmedUndo(api, isSourceBlock, wanted, onTransaction) {
78
+ var _api$history;
79
+ var undo = api === null || api === void 0 || (_api$history = api.history) === null || _api$history === void 0 ? void 0 : _api$history.commands.undo;
80
+ if (!undo || !(api !== null && api !== void 0 && api.core)) {
81
+ return false;
82
+ }
83
+ var applied = false;
84
+ api.core.actions.execute(function (_ref2) {
85
+ var tr = _ref2.tr;
86
+ var undoTr = undo({
87
+ tr: tr
88
+ });
89
+ if (!undoTr || !removesRequestedBlocks(undoTr, isSourceBlock, wanted)) {
90
+ return null;
91
+ }
92
+ undoTr.setMeta('isConfirmedSyncBlockDeletion', true);
93
+ onTransaction(undoTr);
94
+ applied = true;
95
+ return undoTr;
96
+ });
97
+ return applied;
98
+ };
99
+ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(_ref3) {
100
+ var api = _ref3.api,
101
+ confirmationTransactionRef = _ref3.confirmationTransactionRef,
102
+ deletionReason = _ref3.deletionReason,
103
+ feedbackCallbacks = _ref3.feedbackCallbacks,
104
+ mechanism = _ref3.mechanism,
105
+ removed = _ref3.removed,
106
+ syncBlockStore = _ref3.syncBlockStore;
107
+ var removedAttrs = removed.map(function (node) {
108
+ return node.attrs;
109
+ });
110
+ var isSourceBlock = syncBlockStore.sourceManager.isSourceBlock;
111
+
38
112
  // Clear potential old pending deletion to retreat the deletion as first attempt
39
113
  syncBlockStore.sourceManager.clearPendingDeletion();
40
114
 
@@ -42,28 +116,49 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
42
116
  // we block the transaction here, and wait for user confirmation to proceed with deletion.
43
117
  // See editor-common/src/sync-block/sync-block-store-manager.ts for how we handle user confirmation and
44
118
  // proceed with deletion.
45
- syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(function (node) {
46
- return node.attrs;
47
- }), deletionReason, function () {
119
+ syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(removedAttrs, deletionReason, function () {
48
120
  var _api$core2;
121
+ var closeBlockMenu = function closeBlockMenu(confirmedTr) {
122
+ if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_blocks_exp_patch_10')) {
123
+ var _api$blockControls;
124
+ // The original transaction's closeMenu meta was rejected along with it above,
125
+ // so re-apply it here to close the block menu once the change actually lands.
126
+ api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 || (_api$blockControls = _api$blockControls.commands) === null || _api$blockControls === void 0 || _api$blockControls.toggleBlockMenu({
127
+ closeMenu: true
128
+ })({
129
+ tr: confirmedTr
130
+ });
131
+ }
132
+ };
133
+
134
+ // Any undo that removes a source block (creation, paste, ...) must restore what was
135
+ // there before. A recomputed whole-node delete drops the converted content and leaves
136
+ // the event on the undo stack, so replay the undo instead (EDITOR-8863).
137
+ var shouldReplayUndo = mechanism === 'undo' && (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_blocks_exp_patch_10');
138
+ if (shouldReplayUndo && replayConfirmedUndo(api, isSourceBlock, removedAttrs, function (undoTr) {
139
+ closeBlockMenu(undoTr);
140
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(undoTr);
141
+ })) {
142
+ return;
143
+ }
144
+
49
145
  // Recompute the delete fresh from the live document instead of
50
146
  // replaying a stashed-and-rebased transaction. The stash/rebase
51
147
  // approach produced stale, schema-invalid transactions when the
52
148
  // document changed shape (local edits or remote collab) while the
53
149
  // confirmation modal was open — the dominant signature being
54
150
  // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
55
- api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
56
- var tr = _ref2.tr;
151
+ api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref4) {
152
+ var tr = _ref4.tr;
57
153
  // EDITOR-8230: unsync must remove only the sync wrapper and keep the block's
58
154
  // content inline, whereas delete removes the whole block. Before this branch,
59
155
  // the confirm path always recomputed a full delete, so unsyncing a source block
60
156
  // silently deleted its content. When the deletion was actually triggered by
61
157
  // unsync, recompute an unwrap (replaceWith content) instead of a delete.
62
- var recomputedTr = deletionReason === 'source-block-unsynced' ? (0, _recomputeDeleteTransaction.recomputeUnsyncTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
63
- return node.attrs;
64
- })) : (0, _recomputeDeleteTransaction.recomputeDeleteTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
65
- return node.attrs;
66
- }));
158
+ // The same applies when the undo could not be replayed (the undo stack changed
159
+ // while the backend delete was in flight): the backend delete has already
160
+ // succeeded, so drop the wrapper but keep the content (EDITOR-8863).
161
+ var recomputedTr = deletionReason === 'source-block-unsynced' || shouldReplayUndo ? (0, _recomputeDeleteTransaction.recomputeUnsyncTransaction)(tr, isSourceBlock, removedAttrs) : (0, _recomputeDeleteTransaction.recomputeDeleteTransaction)(tr, isSourceBlock, removedAttrs);
67
162
 
68
163
  // The target node(s) no longer exist in the live document (e.g.
69
164
  // a remote collaborator already removed them). There is nothing
@@ -74,16 +169,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
74
169
  if (!recomputedTr) {
75
170
  return null;
76
171
  }
77
- if ((0, _fg.fg)('platform_editor_blocks_patch_10')) {
78
- var _api$blockControls;
79
- // The original transaction's closeMenu meta was rejected along with it above,
80
- // so re-apply it here to close the block menu once the delete actually lands.
81
- api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 || (_api$blockControls = _api$blockControls.commands) === null || _api$blockControls === void 0 || _api$blockControls.toggleBlockMenu({
82
- closeMenu: true
83
- })({
84
- tr: recomputedTr
85
- });
86
- }
172
+ closeBlockMenu(recomputedTr);
87
173
  recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
88
174
  feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(recomputedTr);
89
175
  if (!recomputedTr.getMeta(_utils.pmHistoryPluginKey)) {
@@ -96,8 +182,8 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
96
182
  }, function (success) {
97
183
  var _api$core3;
98
184
  feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteCompleted(success);
99
- api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function (_ref3) {
100
- var tr = _ref3.tr;
185
+ api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function (_ref5) {
186
+ var tr = _ref5.tr;
101
187
  var newState;
102
188
  if (!success) {
103
189
  newState = {
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.recomputeUnsyncTransaction = exports.recomputeDeleteTransaction = void 0;
6
+ exports.recomputeUnsyncTransaction = exports.recomputeDeleteTransaction = exports.matchesRequestedBlock = void 0;
7
7
  var _model = require("@atlaskit/editor-prosemirror/model");
8
8
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
9
9
  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; } }
@@ -17,7 +17,7 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
17
17
  * requested block's `localId` first and only fall back to its `resourceId` when
18
18
  * the `localId` has been regenerated (so it no longer matches the live node).
19
19
  */
20
- var matchesRequestedBlock = function matchesRequestedBlock(node, wanted) {
20
+ var matchesRequestedBlock = exports.matchesRequestedBlock = function matchesRequestedBlock(node, wanted) {
21
21
  var _ref = node.attrs,
22
22
  localId = _ref.localId,
23
23
  resourceId = _ref.resourceId;
@@ -1,10 +1,12 @@
1
1
  import { defaultSchema } from '@atlaskit/adf-schema/schema-default';
2
2
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
3
3
  import { copyDomNodeWithResult, toDOM } from '@atlaskit/editor-common/copy-button';
4
+ import { getSourceNodesFromSelectionRange } from '@atlaskit/editor-common/selection';
4
5
  import { DOMSerializer, Fragment } from '@atlaskit/editor-prosemirror/model';
5
6
  import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
6
7
  import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
7
8
  import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
9
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
8
10
  import { fg } from '@atlaskit/platform-feature-flags/fg';
9
11
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
10
12
  import { creationMetaKey, deleteMechanismMetaKey, syncedBlockPluginKey } from '../pm-plugins/main';
@@ -86,6 +88,7 @@ export const createSyncedBlock = ({
86
88
  // set on the final transaction before returning (see below), since the
87
89
  // typeahead path may reassign `tr` and drop meta set here.
88
90
  const createdEmpty = tr.selection.empty;
91
+ let nodeTypes;
89
92
 
90
93
  // If the selection is empty, we want to insert the sync block on a new line
91
94
  if (tr.selection.empty) {
@@ -127,6 +130,9 @@ export const createSyncedBlock = ({
127
130
  });
128
131
  return false;
129
132
  }
133
+ if (isExperimentEnabled('platform_editor_sync_block_node_types')) {
134
+ nodeTypes = [...new Set(getSourceNodesFromSelectionRange(tr, tr.selection).map(node => node.type.name))].sort();
135
+ }
130
136
  const attrs = syncBlockStore.sourceManager.generateBodiedSyncBlockAttrs();
131
137
  const newBodiedSyncBlockNode = bodiedSyncBlock.createAndFill(attrs, conversionInfo.contentToInclude, fg('platform_editor_blocks_patch_8') && conversionInfo.breakoutMark ? [conversionInfo.breakoutMark] : undefined);
132
138
  if (!newBodiedSyncBlockNode) {
@@ -148,12 +154,12 @@ export const createSyncedBlock = ({
148
154
  tr = placeCaretInsideBodiedSyncBlock(tr, newBodiedSyncBlockNode.attrs.localId);
149
155
  }
150
156
 
151
- // Stash creation-type signals on the final transaction (after any typeahead
152
- // reassignment). Set unconditionally — the store manager only reads it behind
153
- // the feature gate.
157
+ // Stash creation-type signals on the final transaction after any typeahead
158
+ // reassignment. nodeTypes is populated only for gated non-empty conversions.
154
159
  tr.setMeta(creationMetaKey, {
155
160
  createdEmpty,
156
- inputMethod
161
+ inputMethod,
162
+ nodeTypes
157
163
  });
158
164
  return tr;
159
165
  };
@@ -6,11 +6,13 @@ 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 { Slice } from '@atlaskit/editor-prosemirror/model';
9
10
  import { NodeSelection, PluginKey } from '@atlaskit/editor-prosemirror/state';
10
11
  import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
11
12
  import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
12
13
  import { convertPMNodesToSyncBlockNodes } from '@atlaskit/editor-synced-block-provider';
13
14
  import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
15
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
14
16
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
15
17
  import { bodiedSyncBlockNodeView } from '../nodeviews/bodiedSyncedBlock';
16
18
  import { SyncBlock as SyncBlockView } from '../nodeviews/syncedBlock';
@@ -37,7 +39,7 @@ export const deleteMechanismMetaKey = new PluginKey('syncedBlockDeleteMechanism'
37
39
  const syncedBlockPromptedFeedbackMetaKey = new PluginKey('syncedBlockPromptedFeedback');
38
40
 
39
41
  /**
40
- * Creation-type signals set by {@link createSyncedBlock} on the creating
42
+ * Creation analytics signals set by {@link createSyncedBlock} on the creating
41
43
  * transaction. The async creation handler forwards them to the store manager,
42
44
  * which attaches them to the `syncedBlockCreate` event. A dedicated key keeps
43
45
  * this transient signal isolated from plugin state.
@@ -292,18 +294,27 @@ const filterTransactionOnline = ({
292
294
  blockType: 'source',
293
295
  entryPoint
294
296
  } : undefined;
297
+ let feedbackCallbacks;
295
298
  if (feedbackContext) {
296
299
  const sourceAttempt = ctx.queueSourceFeedback(feedbackContext);
297
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, {
300
+ feedbackCallbacks = {
298
301
  onDeleteCompleted: success => ctx.completeSourceDeletion(sourceAttempt, success),
299
302
  onDeleteTransaction: deleteTr => deleteTr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
300
303
  context: feedbackContext,
301
304
  sourceAttempt
302
305
  }),
303
306
  onDestroy: () => ctx.clearSourceFeedback(sourceAttempt)
304
- });
307
+ };
305
308
  }
306
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, undefined);
309
+ return handleBodiedSyncBlockRemoval({
310
+ removed: bodiedSyncBlockRemoved,
311
+ syncBlockStore,
312
+ api,
313
+ confirmationTransactionRef,
314
+ deletionReason: getDeleteReason(tr),
315
+ mechanism,
316
+ feedbackCallbacks
317
+ });
307
318
  }
308
319
  if (bodiedSyncBlockAdded.length > 0) {
309
320
  if (tr.getMeta(pmHistoryPluginKey)) {
@@ -323,7 +334,7 @@ const filterTransactionOnline = ({
323
334
  return true;
324
335
  }
325
336
 
326
- // Forward creation-type signals captured by createSyncedBlock onto the async
337
+ // Forward creation analytics signals captured by createSyncedBlock onto the async
327
338
  // `syncedBlockCreate` success event.
328
339
  const creationMeta = tr.getMeta(creationMetaKey);
329
340
  handleBodiedSyncBlockCreation(bodiedSyncBlockAdded, state, api, creationMeta);
@@ -1068,16 +1079,38 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
1068
1079
  added
1069
1080
  } = trackSyncBlocks(syncBlockStore.sourceManager.isSourceBlock, tr, oldState);
1070
1081
  if (added.length > 0) {
1071
- // Delete bodiedSyncBlock if it's originated from history, i.e. redo creation
1072
- // See filterTransaction above for more details
1082
+ // A bodiedSyncBlock must not be (re)created from history, i.e. redo of a
1083
+ // creation (see filterTransaction above). Unwrap it instead of deleting it:
1084
+ // deleting the whole node also dropped the content the block was converted
1085
+ // from (e.g. a table), and undoing that delete re-added the block and hit
1086
+ // this guard again, so the content was unrecoverable (EDITOR-8863).
1073
1087
  const {
1074
1088
  tr
1075
1089
  } = newState;
1076
- added.forEach(node => {
1077
- if (node.from !== undefined && node.to !== undefined) {
1078
- tr.delete(node.from, node.to);
1079
- }
1080
- });
1090
+ if (isExperimentEnabled('platform_editor_blocks_exp_patch_10')) {
1091
+ // Unwrap in reverse document order so earlier replacements keep later
1092
+ // positions valid.
1093
+ [...added].sort((a, b) => {
1094
+ var _b$from, _a$from;
1095
+ return ((_b$from = b.from) !== null && _b$from !== void 0 ? _b$from : 0) - ((_a$from = a.from) !== null && _a$from !== void 0 ? _a$from : 0);
1096
+ }).forEach(({
1097
+ node,
1098
+ from,
1099
+ to
1100
+ }) => {
1101
+ if (node && from !== undefined && to !== undefined) {
1102
+ // This is ProseMirror's Transaction.replace (a document edit), not String.replace.
1103
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
1104
+ tr.replace(from, to, new Slice(node.content, 0, 0));
1105
+ }
1106
+ });
1107
+ } else {
1108
+ added.forEach(node => {
1109
+ if (node.from !== undefined && node.to !== undefined) {
1110
+ tr.delete(node.from, node.to);
1111
+ }
1112
+ });
1113
+ }
1081
1114
  return tr;
1082
1115
  }
1083
1116
  }
@@ -2,7 +2,7 @@ import { TextSelection } from '@atlaskit/editor-prosemirror/state';
2
2
  import { FLAG_ID } from '../../types';
3
3
  import { syncedBlockPluginKey } from '../main';
4
4
  import { deferDispatch } from './utils';
5
- const onRetry = (api, resourceId) => {
5
+ const onRetry = (api, resourceId, enrichment) => {
6
6
  return () => {
7
7
  var _api$core, _api$core2;
8
8
  api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.focus();
@@ -19,7 +19,7 @@ const onRetry = (api, resourceId) => {
19
19
  tr.setSelection(TextSelection.create(tr.doc, from, to)).setMeta(syncedBlockPluginKey, {
20
20
  activeFlag: false
21
21
  });
22
- api === null || api === void 0 ? void 0 : (_api$syncedBlock2 = api.syncedBlock) === null || _api$syncedBlock2 === void 0 ? void 0 : _api$syncedBlock2.commands.insertSyncedBlock()({
22
+ api === null || api === void 0 ? void 0 : (_api$syncedBlock2 = api.syncedBlock) === null || _api$syncedBlock2 === void 0 ? void 0 : _api$syncedBlock2.commands.insertSyncedBlock(enrichment === null || enrichment === void 0 ? void 0 : enrichment.inputMethod)({
23
23
  tr
24
24
  });
25
25
  return tr;
@@ -67,7 +67,7 @@ const buildRevertCreationTr = (tr, pos) => {
67
67
  *
68
68
  * Save the new bodiedSyncBlock to backend with empty content and handles revert (if failed) and retry flow.
69
69
  *
70
- * @param enrichment optional creation-type signals from `createSyncedBlock`,
70
+ * @param enrichment optional creation analytics signals from `createSyncedBlock`,
71
71
  * forwarded to the store manager to attach to the `syncedBlockCreate` event.
72
72
  */
73
73
  export const handleBodiedSyncBlockCreation = (bodiedSyncBlockAdded, editorState, api, enrichment) => {
@@ -121,7 +121,7 @@ export const handleBodiedSyncBlockCreation = (bodiedSyncBlockAdded, editorState,
121
121
  return revertTr.setMeta('isConfirmedSyncBlockDeletion', true).setMeta('addToHistory', false).setMeta(syncedBlockPluginKey, {
122
122
  activeFlag: {
123
123
  id: FLAG_ID.CANNOT_CREATE_SYNC_BLOCK,
124
- onRetry: onRetry(api, resourceId),
124
+ onRetry: onRetry(api, resourceId, enrichment),
125
125
  onDismissed: tr => tr.setMeta(syncedBlockPluginKey, {
126
126
  ...tr.getMeta(syncedBlockPluginKey),
127
127
  retryCreationPos: {