@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.
@@ -1,8 +1,8 @@
1
1
  import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
2
- import { fg } from '@atlaskit/platform-feature-flags/fg';
2
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
3
3
  import { FLAG_ID } from '../../types';
4
4
  import { syncedBlockPluginKey } from '../main';
5
- import { recomputeDeleteTransaction, recomputeUnsyncTransaction } from './recompute-delete-transaction';
5
+ import { matchesRequestedBlock, recomputeDeleteTransaction, recomputeUnsyncTransaction } from './recompute-delete-transaction';
6
6
  const onRetry = (api, syncBlockStore) => () => {
7
7
  var _api$core;
8
8
  api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
@@ -21,7 +21,77 @@ const onDismissed = syncBlockStore => tr => {
21
21
  bodiedSyncBlockDeletionStatus: 'none'
22
22
  });
23
23
  };
24
- export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism, feedbackCallbacks) => {
24
+ const topLevelSourceBlocks = (doc, isSourceBlock) => {
25
+ const blocks = [];
26
+ doc.forEach(node => {
27
+ if (isSourceBlock(node)) {
28
+ blocks.push(node);
29
+ }
30
+ });
31
+ return blocks;
32
+ };
33
+
34
+ /**
35
+ * Whether `tr` removes every block in `wanted` from the document. Identity follows
36
+ * `matchesRequestedBlock` (localId, falling back to resourceId) so this agrees with the
37
+ * recompute helpers.
38
+ */
39
+ const removesRequestedBlocks = (tr, isSourceBlock, wanted) => {
40
+ const before = topLevelSourceBlocks(tr.before, isSourceBlock);
41
+ const after = topLevelSourceBlocks(tr.doc, isSourceBlock);
42
+ return wanted.every(block => before.some(node => matchesRequestedBlock(node, block)) && !after.some(node => matchesRequestedBlock(node, block)));
43
+ };
44
+
45
+ /**
46
+ * Replay the undo that removed `wanted` once the source deletion has been confirmed and
47
+ * persisted (EDITOR-8863).
48
+ *
49
+ * The original undo transaction was rejected by `filterTransaction` so the confirmation could
50
+ * run, which leaves the history event on top of the undo stack. Building the undo again from
51
+ * the live state (via the history plugin) restores whatever the block was converted from
52
+ * (e.g. a table), unlike a recomputed whole-node delete which drops that content.
53
+ *
54
+ * Guard: only dispatch if that undo still removes the same source block(s). If the user
55
+ * edited in the meantime the top of the stack is no longer that event, so return `false` and
56
+ * let the caller fall back to the recomputed unwrap.
57
+ */
58
+ const replayConfirmedUndo = (api, isSourceBlock, wanted, onTransaction) => {
59
+ var _api$history;
60
+ const undo = api === null || api === void 0 ? void 0 : (_api$history = api.history) === null || _api$history === void 0 ? void 0 : _api$history.commands.undo;
61
+ if (!undo || !(api !== null && api !== void 0 && api.core)) {
62
+ return false;
63
+ }
64
+ let applied = false;
65
+ api.core.actions.execute(({
66
+ tr
67
+ }) => {
68
+ const undoTr = undo({
69
+ tr
70
+ });
71
+ if (!undoTr || !removesRequestedBlocks(undoTr, isSourceBlock, wanted)) {
72
+ return null;
73
+ }
74
+ undoTr.setMeta('isConfirmedSyncBlockDeletion', true);
75
+ onTransaction(undoTr);
76
+ applied = true;
77
+ return undoTr;
78
+ });
79
+ return applied;
80
+ };
81
+ export const handleBodiedSyncBlockRemoval = ({
82
+ api,
83
+ confirmationTransactionRef,
84
+ deletionReason,
85
+ feedbackCallbacks,
86
+ mechanism,
87
+ removed,
88
+ syncBlockStore
89
+ }) => {
90
+ const removedAttrs = removed.map(node => node.attrs);
91
+ const {
92
+ isSourceBlock
93
+ } = syncBlockStore.sourceManager;
94
+
25
95
  // Clear potential old pending deletion to retreat the deletion as first attempt
26
96
  syncBlockStore.sourceManager.clearPendingDeletion();
27
97
 
@@ -29,8 +99,32 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
29
99
  // we block the transaction here, and wait for user confirmation to proceed with deletion.
30
100
  // See editor-common/src/sync-block/sync-block-store-manager.ts for how we handle user confirmation and
31
101
  // proceed with deletion.
32
- syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(node => node.attrs), deletionReason, () => {
102
+ syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(removedAttrs, deletionReason, () => {
33
103
  var _api$core2;
104
+ const closeBlockMenu = confirmedTr => {
105
+ if (isExperimentEnabled('platform_editor_blocks_exp_patch_10')) {
106
+ var _api$blockControls, _api$blockControls$co;
107
+ // The original transaction's closeMenu meta was rejected along with it above,
108
+ // so re-apply it here to close the block menu once the change actually lands.
109
+ api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : (_api$blockControls$co = _api$blockControls.commands) === null || _api$blockControls$co === void 0 ? void 0 : _api$blockControls$co.toggleBlockMenu({
110
+ closeMenu: true
111
+ })({
112
+ tr: confirmedTr
113
+ });
114
+ }
115
+ };
116
+
117
+ // Any undo that removes a source block (creation, paste, ...) must restore what was
118
+ // there before. A recomputed whole-node delete drops the converted content and leaves
119
+ // the event on the undo stack, so replay the undo instead (EDITOR-8863).
120
+ const shouldReplayUndo = mechanism === 'undo' && isExperimentEnabled('platform_editor_blocks_exp_patch_10');
121
+ if (shouldReplayUndo && replayConfirmedUndo(api, isSourceBlock, removedAttrs, undoTr => {
122
+ closeBlockMenu(undoTr);
123
+ feedbackCallbacks === null || feedbackCallbacks === void 0 ? void 0 : feedbackCallbacks.onDeleteTransaction(undoTr);
124
+ })) {
125
+ return;
126
+ }
127
+
34
128
  // Recompute the delete fresh from the live document instead of
35
129
  // replaying a stashed-and-rebased transaction. The stash/rebase
36
130
  // approach produced stale, schema-invalid transactions when the
@@ -45,7 +139,10 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
45
139
  // the confirm path always recomputed a full delete, so unsyncing a source block
46
140
  // silently deleted its content. When the deletion was actually triggered by
47
141
  // unsync, recompute an unwrap (replaceWith content) instead of a delete.
48
- const recomputedTr = deletionReason === 'source-block-unsynced' ? recomputeUnsyncTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs)) : recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs));
142
+ // The same applies when the undo could not be replayed (the undo stack changed
143
+ // while the backend delete was in flight): the backend delete has already
144
+ // succeeded, so drop the wrapper but keep the content (EDITOR-8863).
145
+ const recomputedTr = deletionReason === 'source-block-unsynced' || shouldReplayUndo ? recomputeUnsyncTransaction(tr, isSourceBlock, removedAttrs) : recomputeDeleteTransaction(tr, isSourceBlock, removedAttrs);
49
146
 
50
147
  // The target node(s) no longer exist in the live document (e.g.
51
148
  // a remote collaborator already removed them). There is nothing
@@ -56,16 +153,7 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
56
153
  if (!recomputedTr) {
57
154
  return null;
58
155
  }
59
- if (fg('platform_editor_blocks_patch_10')) {
60
- var _api$blockControls, _api$blockControls$co;
61
- // The original transaction's closeMenu meta was rejected along with it above,
62
- // so re-apply it here to close the block menu once the delete actually lands.
63
- api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : (_api$blockControls$co = _api$blockControls.commands) === null || _api$blockControls$co === void 0 ? void 0 : _api$blockControls$co.toggleBlockMenu({
64
- closeMenu: true
65
- })({
66
- tr: recomputedTr
67
- });
68
- }
156
+ closeBlockMenu(recomputedTr);
69
157
  recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
70
158
  feedbackCallbacks === null || feedbackCallbacks === void 0 ? void 0 : feedbackCallbacks.onDeleteTransaction(recomputedTr);
71
159
  if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
@@ -8,7 +8,7 @@ import { Slice } from '@atlaskit/editor-prosemirror/model';
8
8
  * requested block's `localId` first and only fall back to its `resourceId` when
9
9
  * the `localId` has been regenerated (so it no longer matches the live node).
10
10
  */
11
- const matchesRequestedBlock = (node, wanted) => {
11
+ export const matchesRequestedBlock = (node, wanted) => {
12
12
  const {
13
13
  localId,
14
14
  resourceId
@@ -1,13 +1,16 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
3
  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
4
  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
5
  import { defaultSchema } from '@atlaskit/adf-schema/schema-default';
5
6
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
6
7
  import { copyDomNodeWithResult, toDOM } from '@atlaskit/editor-common/copy-button';
8
+ import { getSourceNodesFromSelectionRange } from '@atlaskit/editor-common/selection';
7
9
  import { DOMSerializer, Fragment } from '@atlaskit/editor-prosemirror/model';
8
10
  import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
9
11
  import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
10
12
  import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
13
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
11
14
  import { fg } from '@atlaskit/platform-feature-flags/fg';
12
15
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
13
16
  import { creationMetaKey, deleteMechanismMetaKey, syncedBlockPluginKey } from '../pm-plugins/main';
@@ -83,6 +86,7 @@ export var createSyncedBlock = function createSyncedBlock(_ref) {
83
86
  // set on the final transaction before returning (see below), since the
84
87
  // typeahead path may reassign `tr` and drop meta set here.
85
88
  var createdEmpty = tr.selection.empty;
89
+ var nodeTypes;
86
90
 
87
91
  // If the selection is empty, we want to insert the sync block on a new line
88
92
  if (tr.selection.empty) {
@@ -124,6 +128,11 @@ export var createSyncedBlock = function createSyncedBlock(_ref) {
124
128
  });
125
129
  return false;
126
130
  }
131
+ if (isExperimentEnabled('platform_editor_sync_block_node_types')) {
132
+ nodeTypes = _toConsumableArray(new Set(getSourceNodesFromSelectionRange(tr, tr.selection).map(function (node) {
133
+ return node.type.name;
134
+ }))).sort();
135
+ }
127
136
  var _attrs = syncBlockStore.sourceManager.generateBodiedSyncBlockAttrs();
128
137
  var _newBodiedSyncBlockNode = bodiedSyncBlock.createAndFill(_attrs, conversionInfo.contentToInclude, fg('platform_editor_blocks_patch_8') && conversionInfo.breakoutMark ? [conversionInfo.breakoutMark] : undefined);
129
138
  if (!_newBodiedSyncBlockNode) {
@@ -145,12 +154,12 @@ export var createSyncedBlock = function createSyncedBlock(_ref) {
145
154
  tr = placeCaretInsideBodiedSyncBlock(tr, _newBodiedSyncBlockNode.attrs.localId);
146
155
  }
147
156
 
148
- // Stash creation-type signals on the final transaction (after any typeahead
149
- // reassignment). Set unconditionally — the store manager only reads it behind
150
- // 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.
151
159
  tr.setMeta(creationMetaKey, {
152
160
  createdEmpty: createdEmpty,
153
- inputMethod: inputMethod
161
+ inputMethod: inputMethod,
162
+ nodeTypes: nodeTypes
154
163
  });
155
164
  return tr;
156
165
  };
@@ -1,3 +1,4 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
1
2
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
3
  import _createClass from "@babel/runtime/helpers/createClass";
3
4
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
@@ -15,11 +16,13 @@ import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
15
16
  import { BodiedSyncBlockSharedCssClassName, SyncBlockStateCssClassName } from '@atlaskit/editor-common/sync-block';
16
17
  import { mapSlice, pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
17
18
  import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
19
+ import { Slice } from '@atlaskit/editor-prosemirror/model';
18
20
  import { NodeSelection, PluginKey } from '@atlaskit/editor-prosemirror/state';
19
21
  import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
20
22
  import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
21
23
  import { convertPMNodesToSyncBlockNodes } from '@atlaskit/editor-synced-block-provider';
22
24
  import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
25
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
23
26
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
24
27
  import { bodiedSyncBlockNodeView } from '../nodeviews/bodiedSyncedBlock';
25
28
  import { SyncBlock as SyncBlockView } from '../nodeviews/syncedBlock';
@@ -46,7 +49,7 @@ export var deleteMechanismMetaKey = new PluginKey('syncedBlockDeleteMechanism');
46
49
  var syncedBlockPromptedFeedbackMetaKey = new PluginKey('syncedBlockPromptedFeedback');
47
50
 
48
51
  /**
49
- * Creation-type signals set by {@link createSyncedBlock} on the creating
52
+ * Creation analytics signals set by {@link createSyncedBlock} on the creating
50
53
  * transaction. The async creation handler forwards them to the store manager,
51
54
  * which attaches them to the `syncedBlockCreate` event. A dedicated key keeps
52
55
  * this transient signal isolated from plugin state.
@@ -312,9 +315,10 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
312
315
  blockType: 'source',
313
316
  entryPoint: entryPoint
314
317
  } : undefined;
318
+ var feedbackCallbacks;
315
319
  if (feedbackContext) {
316
320
  var sourceAttempt = ctx.queueSourceFeedback(feedbackContext);
317
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, {
321
+ feedbackCallbacks = {
318
322
  onDeleteCompleted: function onDeleteCompleted(success) {
319
323
  return ctx.completeSourceDeletion(sourceAttempt, success);
320
324
  },
@@ -327,9 +331,17 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
327
331
  onDestroy: function onDestroy() {
328
332
  return ctx.clearSourceFeedback(sourceAttempt);
329
333
  }
330
- });
334
+ };
331
335
  }
332
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, undefined);
336
+ return handleBodiedSyncBlockRemoval({
337
+ removed: bodiedSyncBlockRemoved,
338
+ syncBlockStore: syncBlockStore,
339
+ api: api,
340
+ confirmationTransactionRef: confirmationTransactionRef,
341
+ deletionReason: getDeleteReason(tr),
342
+ mechanism: mechanism,
343
+ feedbackCallbacks: feedbackCallbacks
344
+ });
333
345
  }
334
346
  if (bodiedSyncBlockAdded.length > 0) {
335
347
  if (tr.getMeta(pmHistoryPluginKey)) {
@@ -349,7 +361,7 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
349
361
  return true;
350
362
  }
351
363
 
352
- // Forward creation-type signals captured by createSyncedBlock onto the async
364
+ // Forward creation analytics signals captured by createSyncedBlock onto the async
353
365
  // `syncedBlockCreate` success event.
354
366
  var creationMeta = tr.getMeta(creationMetaKey);
355
367
  handleBodiedSyncBlockCreation(bodiedSyncBlockAdded, state, api, creationMeta);
@@ -1134,14 +1146,35 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
1134
1146
  var _trackSyncBlocks6 = trackSyncBlocks(syncBlockStore.sourceManager.isSourceBlock, tr, oldState),
1135
1147
  added = _trackSyncBlocks6.added;
1136
1148
  if (added.length > 0) {
1137
- // Delete bodiedSyncBlock if it's originated from history, i.e. redo creation
1138
- // See filterTransaction above for more details
1149
+ // A bodiedSyncBlock must not be (re)created from history, i.e. redo of a
1150
+ // creation (see filterTransaction above). Unwrap it instead of deleting it:
1151
+ // deleting the whole node also dropped the content the block was converted
1152
+ // from (e.g. a table), and undoing that delete re-added the block and hit
1153
+ // this guard again, so the content was unrecoverable (EDITOR-8863).
1139
1154
  var _tr = newState.tr;
1140
- added.forEach(function (node) {
1141
- if (node.from !== undefined && node.to !== undefined) {
1142
- _tr.delete(node.from, node.to);
1143
- }
1144
- });
1155
+ if (isExperimentEnabled('platform_editor_blocks_exp_patch_10')) {
1156
+ // Unwrap in reverse document order so earlier replacements keep later
1157
+ // positions valid.
1158
+ _toConsumableArray(added).sort(function (a, b) {
1159
+ var _b$from, _a$from;
1160
+ 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);
1161
+ }).forEach(function (_ref12) {
1162
+ var node = _ref12.node,
1163
+ from = _ref12.from,
1164
+ to = _ref12.to;
1165
+ if (node && from !== undefined && to !== undefined) {
1166
+ // This is ProseMirror's Transaction.replace (a document edit), not String.replace.
1167
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
1168
+ _tr.replace(from, to, new Slice(node.content, 0, 0));
1169
+ }
1170
+ });
1171
+ } else {
1172
+ added.forEach(function (node) {
1173
+ if (node.from !== undefined && node.to !== undefined) {
1174
+ _tr.delete(node.from, node.to);
1175
+ }
1176
+ });
1177
+ }
1145
1178
  return {
1146
1179
  v: _tr
1147
1180
  };
@@ -5,7 +5,7 @@ import { TextSelection } from '@atlaskit/editor-prosemirror/state';
5
5
  import { FLAG_ID } from '../../types';
6
6
  import { syncedBlockPluginKey } from '../main';
7
7
  import { deferDispatch } from './utils';
8
- var onRetry = function onRetry(api, resourceId) {
8
+ var onRetry = function onRetry(api, resourceId, enrichment) {
9
9
  return function () {
10
10
  var _api$core, _api$core2;
11
11
  api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.focus();
@@ -21,7 +21,7 @@ var onRetry = function onRetry(api, resourceId) {
21
21
  tr.setSelection(TextSelection.create(tr.doc, from, to)).setMeta(syncedBlockPluginKey, {
22
22
  activeFlag: false
23
23
  });
24
- api === null || api === void 0 || (_api$syncedBlock2 = api.syncedBlock) === null || _api$syncedBlock2 === void 0 || _api$syncedBlock2.commands.insertSyncedBlock()({
24
+ 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)({
25
25
  tr: tr
26
26
  });
27
27
  return tr;
@@ -69,7 +69,7 @@ var buildRevertCreationTr = function buildRevertCreationTr(tr, pos) {
69
69
  *
70
70
  * Save the new bodiedSyncBlock to backend with empty content and handles revert (if failed) and retry flow.
71
71
  *
72
- * @param enrichment optional creation-type signals from `createSyncedBlock`,
72
+ * @param enrichment optional creation analytics signals from `createSyncedBlock`,
73
73
  * forwarded to the store manager to attach to the `syncedBlockCreate` event.
74
74
  */
75
75
  export var handleBodiedSyncBlockCreation = function handleBodiedSyncBlockCreation(bodiedSyncBlockAdded, editorState, api, enrichment) {
@@ -120,7 +120,7 @@ export var handleBodiedSyncBlockCreation = function handleBodiedSyncBlockCreatio
120
120
  return revertTr.setMeta('isConfirmedSyncBlockDeletion', true).setMeta('addToHistory', false).setMeta(syncedBlockPluginKey, {
121
121
  activeFlag: {
122
122
  id: FLAG_ID.CANNOT_CREATE_SYNC_BLOCK,
123
- onRetry: onRetry(api, resourceId),
123
+ onRetry: onRetry(api, resourceId, enrichment),
124
124
  onDismissed: function onDismissed(tr) {
125
125
  return tr.setMeta(syncedBlockPluginKey, _objectSpread(_objectSpread({}, tr.getMeta(syncedBlockPluginKey)), {}, {
126
126
  retryCreationPos: {
@@ -2,10 +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/fg';
5
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
6
6
  import { FLAG_ID } from '../../types';
7
7
  import { syncedBlockPluginKey } from '../main';
8
- import { recomputeDeleteTransaction, recomputeUnsyncTransaction } from './recompute-delete-transaction';
8
+ import { matchesRequestedBlock, recomputeDeleteTransaction, recomputeUnsyncTransaction } from './recompute-delete-transaction';
9
9
  var onRetry = function onRetry(api, syncBlockStore) {
10
10
  return function () {
11
11
  var _api$core;
@@ -27,7 +27,81 @@ var onDismissed = function onDismissed(syncBlockStore) {
27
27
  });
28
28
  };
29
29
  };
30
- export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism, feedbackCallbacks) {
30
+ var topLevelSourceBlocks = function topLevelSourceBlocks(doc, isSourceBlock) {
31
+ var blocks = [];
32
+ doc.forEach(function (node) {
33
+ if (isSourceBlock(node)) {
34
+ blocks.push(node);
35
+ }
36
+ });
37
+ return blocks;
38
+ };
39
+
40
+ /**
41
+ * Whether `tr` removes every block in `wanted` from the document. Identity follows
42
+ * `matchesRequestedBlock` (localId, falling back to resourceId) so this agrees with the
43
+ * recompute helpers.
44
+ */
45
+ var removesRequestedBlocks = function removesRequestedBlocks(tr, isSourceBlock, wanted) {
46
+ var before = topLevelSourceBlocks(tr.before, isSourceBlock);
47
+ var after = topLevelSourceBlocks(tr.doc, isSourceBlock);
48
+ return wanted.every(function (block) {
49
+ return before.some(function (node) {
50
+ return matchesRequestedBlock(node, block);
51
+ }) && !after.some(function (node) {
52
+ return matchesRequestedBlock(node, block);
53
+ });
54
+ });
55
+ };
56
+
57
+ /**
58
+ * Replay the undo that removed `wanted` once the source deletion has been confirmed and
59
+ * persisted (EDITOR-8863).
60
+ *
61
+ * The original undo transaction was rejected by `filterTransaction` so the confirmation could
62
+ * run, which leaves the history event on top of the undo stack. Building the undo again from
63
+ * the live state (via the history plugin) restores whatever the block was converted from
64
+ * (e.g. a table), unlike a recomputed whole-node delete which drops that content.
65
+ *
66
+ * Guard: only dispatch if that undo still removes the same source block(s). If the user
67
+ * edited in the meantime the top of the stack is no longer that event, so return `false` and
68
+ * let the caller fall back to the recomputed unwrap.
69
+ */
70
+ var replayConfirmedUndo = function replayConfirmedUndo(api, isSourceBlock, wanted, onTransaction) {
71
+ var _api$history;
72
+ var undo = api === null || api === void 0 || (_api$history = api.history) === null || _api$history === void 0 ? void 0 : _api$history.commands.undo;
73
+ if (!undo || !(api !== null && api !== void 0 && api.core)) {
74
+ return false;
75
+ }
76
+ var applied = false;
77
+ api.core.actions.execute(function (_ref2) {
78
+ var tr = _ref2.tr;
79
+ var undoTr = undo({
80
+ tr: tr
81
+ });
82
+ if (!undoTr || !removesRequestedBlocks(undoTr, isSourceBlock, wanted)) {
83
+ return null;
84
+ }
85
+ undoTr.setMeta('isConfirmedSyncBlockDeletion', true);
86
+ onTransaction(undoTr);
87
+ applied = true;
88
+ return undoTr;
89
+ });
90
+ return applied;
91
+ };
92
+ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(_ref3) {
93
+ var api = _ref3.api,
94
+ confirmationTransactionRef = _ref3.confirmationTransactionRef,
95
+ deletionReason = _ref3.deletionReason,
96
+ feedbackCallbacks = _ref3.feedbackCallbacks,
97
+ mechanism = _ref3.mechanism,
98
+ removed = _ref3.removed,
99
+ syncBlockStore = _ref3.syncBlockStore;
100
+ var removedAttrs = removed.map(function (node) {
101
+ return node.attrs;
102
+ });
103
+ var isSourceBlock = syncBlockStore.sourceManager.isSourceBlock;
104
+
31
105
  // Clear potential old pending deletion to retreat the deletion as first attempt
32
106
  syncBlockStore.sourceManager.clearPendingDeletion();
33
107
 
@@ -35,28 +109,49 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
35
109
  // we block the transaction here, and wait for user confirmation to proceed with deletion.
36
110
  // See editor-common/src/sync-block/sync-block-store-manager.ts for how we handle user confirmation and
37
111
  // proceed with deletion.
38
- syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(function (node) {
39
- return node.attrs;
40
- }), deletionReason, function () {
112
+ syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(removedAttrs, deletionReason, function () {
41
113
  var _api$core2;
114
+ var closeBlockMenu = function closeBlockMenu(confirmedTr) {
115
+ if (isExperimentEnabled('platform_editor_blocks_exp_patch_10')) {
116
+ var _api$blockControls;
117
+ // The original transaction's closeMenu meta was rejected along with it above,
118
+ // so re-apply it here to close the block menu once the change actually lands.
119
+ 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({
120
+ closeMenu: true
121
+ })({
122
+ tr: confirmedTr
123
+ });
124
+ }
125
+ };
126
+
127
+ // Any undo that removes a source block (creation, paste, ...) must restore what was
128
+ // there before. A recomputed whole-node delete drops the converted content and leaves
129
+ // the event on the undo stack, so replay the undo instead (EDITOR-8863).
130
+ var shouldReplayUndo = mechanism === 'undo' && isExperimentEnabled('platform_editor_blocks_exp_patch_10');
131
+ if (shouldReplayUndo && replayConfirmedUndo(api, isSourceBlock, removedAttrs, function (undoTr) {
132
+ closeBlockMenu(undoTr);
133
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(undoTr);
134
+ })) {
135
+ return;
136
+ }
137
+
42
138
  // Recompute the delete fresh from the live document instead of
43
139
  // replaying a stashed-and-rebased transaction. The stash/rebase
44
140
  // approach produced stale, schema-invalid transactions when the
45
141
  // document changed shape (local edits or remote collab) while the
46
142
  // confirmation modal was open — the dominant signature being
47
143
  // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
48
- api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
49
- var tr = _ref2.tr;
144
+ api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref4) {
145
+ var tr = _ref4.tr;
50
146
  // EDITOR-8230: unsync must remove only the sync wrapper and keep the block's
51
147
  // content inline, whereas delete removes the whole block. Before this branch,
52
148
  // the confirm path always recomputed a full delete, so unsyncing a source block
53
149
  // silently deleted its content. When the deletion was actually triggered by
54
150
  // unsync, recompute an unwrap (replaceWith content) instead of a delete.
55
- var recomputedTr = deletionReason === 'source-block-unsynced' ? recomputeUnsyncTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
56
- return node.attrs;
57
- })) : recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
58
- return node.attrs;
59
- }));
151
+ // The same applies when the undo could not be replayed (the undo stack changed
152
+ // while the backend delete was in flight): the backend delete has already
153
+ // succeeded, so drop the wrapper but keep the content (EDITOR-8863).
154
+ var recomputedTr = deletionReason === 'source-block-unsynced' || shouldReplayUndo ? recomputeUnsyncTransaction(tr, isSourceBlock, removedAttrs) : recomputeDeleteTransaction(tr, isSourceBlock, removedAttrs);
60
155
 
61
156
  // The target node(s) no longer exist in the live document (e.g.
62
157
  // a remote collaborator already removed them). There is nothing
@@ -67,16 +162,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
67
162
  if (!recomputedTr) {
68
163
  return null;
69
164
  }
70
- if (fg('platform_editor_blocks_patch_10')) {
71
- var _api$blockControls;
72
- // The original transaction's closeMenu meta was rejected along with it above,
73
- // so re-apply it here to close the block menu once the delete actually lands.
74
- 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({
75
- closeMenu: true
76
- })({
77
- tr: recomputedTr
78
- });
79
- }
165
+ closeBlockMenu(recomputedTr);
80
166
  recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
81
167
  feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(recomputedTr);
82
168
  if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
@@ -89,8 +175,8 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
89
175
  }, function (success) {
90
176
  var _api$core3;
91
177
  feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteCompleted(success);
92
- api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function (_ref3) {
93
- var tr = _ref3.tr;
178
+ api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function (_ref5) {
179
+ var tr = _ref5.tr;
94
180
  var newState;
95
181
  if (!success) {
96
182
  newState = {
@@ -11,7 +11,7 @@ import { Slice } from '@atlaskit/editor-prosemirror/model';
11
11
  * requested block's `localId` first and only fall back to its `resourceId` when
12
12
  * the `localId` has been regenerated (so it no longer matches the live node).
13
13
  */
14
- var matchesRequestedBlock = function matchesRequestedBlock(node, wanted) {
14
+ export var matchesRequestedBlock = function matchesRequestedBlock(node, wanted) {
15
15
  var _ref = node.attrs,
16
16
  localId = _ref.localId,
17
17
  resourceId = _ref.resourceId;
@@ -19,7 +19,7 @@ export declare const syncedBlockPluginKey: PluginKey;
19
19
  */
20
20
  export declare const deleteMechanismMetaKey: PluginKey<DeletionMechanism>;
21
21
  /**
22
- * Creation-type signals set by {@link createSyncedBlock} on the creating
22
+ * Creation analytics signals set by {@link createSyncedBlock} on the creating
23
23
  * transaction. The async creation handler forwards them to the store manager,
24
24
  * which attaches them to the `syncedBlockCreate` event. A dedicated key keeps
25
25
  * this transient signal isolated from plugin state.
@@ -27,6 +27,7 @@ export declare const deleteMechanismMetaKey: PluginKey<DeletionMechanism>;
27
27
  export type SyncedBlockCreationMeta = {
28
28
  createdEmpty?: boolean;
29
29
  inputMethod?: INPUT_METHOD;
30
+ nodeTypes?: string[];
30
31
  };
31
32
  export declare const creationMetaKey: PluginKey<SyncedBlockCreationMeta>;
32
33
  type SyncedBlockPluginState = {
@@ -7,7 +7,7 @@ import type { SyncBlockInfo } from '../../types';
7
7
  *
8
8
  * Save the new bodiedSyncBlock to backend with empty content and handles revert (if failed) and retry flow.
9
9
  *
10
- * @param enrichment optional creation-type signals from `createSyncedBlock`,
10
+ * @param enrichment optional creation analytics signals from `createSyncedBlock`,
11
11
  * forwarded to the store manager to attach to the `syncedBlockCreate` event.
12
12
  */
13
13
  export declare const handleBodiedSyncBlockCreation: (bodiedSyncBlockAdded: SyncBlockInfo[], editorState: EditorState, api: ExtractInjectionAPI<SyncedBlockPlugin> | undefined, enrichment?: CreateSuccessEnrichment) => void;
@@ -7,10 +7,18 @@ import type { SyncBlockInfo } from '../../types';
7
7
  export type TransactionRef = {
8
8
  current: Transaction | undefined;
9
9
  };
10
- type SourceFeedbackCallbacks = {
10
+ export type SourceFeedbackCallbacks = {
11
11
  onDeleteCompleted: (success: boolean) => void;
12
12
  onDeleteTransaction: (tr: Transaction) => void;
13
13
  onDestroy: () => void;
14
14
  };
15
- export declare const handleBodiedSyncBlockRemoval: (bodiedSyncBlockRemoved: SyncBlockInfo[], syncBlockStore: SyncBlockStoreManager, api: ExtractInjectionAPI<SyncedBlockPlugin> | undefined, confirmationTransactionRef: TransactionRef, deletionReason: DeletionReason, mechanism?: DeletionMechanism, feedbackCallbacks?: SourceFeedbackCallbacks) => boolean;
16
- export {};
15
+ export type HandleBodiedSyncBlockRemovalOptions = {
16
+ api: ExtractInjectionAPI<SyncedBlockPlugin> | undefined;
17
+ confirmationTransactionRef: TransactionRef;
18
+ deletionReason: DeletionReason;
19
+ feedbackCallbacks?: SourceFeedbackCallbacks;
20
+ mechanism?: DeletionMechanism;
21
+ removed: SyncBlockInfo[];
22
+ syncBlockStore: SyncBlockStoreManager;
23
+ };
24
+ export declare const handleBodiedSyncBlockRemoval: ({ api, confirmationTransactionRef, deletionReason, feedbackCallbacks, mechanism, removed, syncBlockStore, }: HandleBodiedSyncBlockRemovalOptions) => boolean;
@@ -1,6 +1,16 @@
1
1
  import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
2
2
  import type { Transaction } from '@atlaskit/editor-prosemirror/state';
3
3
  import type { SyncBlockAttrs } from '../../types';
4
+ /**
5
+ * Whether `node` is the source block requested by `wanted`.
6
+ *
7
+ * Matching is done *per requested block* (rather than against global pools of
8
+ * every requested localId/resourceId) so a node cannot be matched by combining
9
+ * one block's `localId` with a different block's `resourceId`. We match on the
10
+ * requested block's `localId` first and only fall back to its `resourceId` when
11
+ * the `localId` has been regenerated (so it no longer matches the live node).
12
+ */
13
+ export declare const matchesRequestedBlock: (node: PMNode, wanted: SyncBlockAttrs) => boolean;
4
14
  /**
5
15
  * Recompute a source `bodiedSyncBlock` delete from the live document.
6
16
  *
@@ -12,6 +12,7 @@ import type { EditorDisabledPlugin } from '@atlaskit/editor-plugin-editor-disabl
12
12
  import type { EditorViewModePlugin } from '@atlaskit/editor-plugin-editor-viewmode';
13
13
  import type { FloatingToolbarPlugin } from '@atlaskit/editor-plugin-floating-toolbar';
14
14
  import type { FocusPlugin } from '@atlaskit/editor-plugin-focus';
15
+ import type { HistoryPlugin } from '@atlaskit/editor-plugin-history';
15
16
  import type { SelectionPlugin } from '@atlaskit/editor-plugin-selection';
16
17
  import type { ToolbarPlugin } from '@atlaskit/editor-plugin-toolbar';
17
18
  import type { UserIntentPlugin } from '@atlaskit/editor-plugin-user-intent';
@@ -136,6 +137,7 @@ export type SyncedBlockPlugin = NextEditorPlugin<'syncedBlock', {
136
137
  OptionalPlugin<ContentFormatPlugin>,
137
138
  OptionalPlugin<UserIntentPlugin>,
138
139
  OptionalPlugin<FocusPlugin>,
140
+ OptionalPlugin<HistoryPlugin>,
139
141
  OptionalPlugin<UiControlRegistryPlugin>
140
142
  ];
141
143
  pluginConfiguration: SyncedBlockPluginOptions | undefined;