@atlaskit/editor-plugin-synced-block 10.4.7 → 10.6.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.
@@ -22,6 +22,7 @@ import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/edi
22
22
  import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
23
23
  import { fg } from '@atlaskit/platform-feature-flags';
24
24
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
25
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
25
26
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
26
27
  import { bodiedSyncBlockNodeView, bodiedSyncBlockNodeViewOld } from '../nodeviews/bodiedSyncedBlock';
27
28
  import { SyncBlock as SyncBlockView } from '../nodeviews/syncedBlock';
@@ -46,6 +47,7 @@ export var syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
46
47
  * `platform_editor_blocks_patch_4`.
47
48
  */
48
49
  export var deleteMechanismMetaKey = new PluginKey('syncedBlockDeleteMechanism');
50
+ var syncedBlockPromptedFeedbackMetaKey = new PluginKey('syncedBlockPromptedFeedback');
49
51
 
50
52
  /**
51
53
  * Creation-type signals set by {@link createSyncedBlock} on the creating
@@ -142,7 +144,7 @@ var getDeleteReason = function getDeleteReason(tr) {
142
144
 
143
145
  /** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
144
146
  var isHistoryMeta = function isHistoryMeta(meta) {
145
- return _typeof(meta) === 'object' && meta !== null && 'redo' in meta;
147
+ return _typeof(meta) === 'object' && meta !== null && 'redo' in meta && typeof meta.redo === 'boolean';
146
148
  };
147
149
 
148
150
  /**
@@ -167,9 +169,16 @@ var isHistoryMeta = function isHistoryMeta(meta) {
167
169
  * selected when the edit was made.
168
170
  */
169
171
  export var getDeleteMechanism = function getDeleteMechanism(tr, state) {
170
- var _selection$node;
172
+ var _selection$node, _selection$node2;
173
+ // Keep the legacy analytics classification unchanged while activation is off.
174
+ // The stricter history, structural-step, and reference-selection handling is
175
+ // only needed by the prompted feedback rollout.
176
+ var isSyncBlockActivationEnabled = expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true);
171
177
  var historyMeta = tr.getMeta(pmHistoryPluginKey);
172
178
  if (historyMeta) {
179
+ if (isSyncBlockActivationEnabled && !isHistoryMeta(historyMeta)) {
180
+ return 'other';
181
+ }
173
182
  return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
174
183
  }
175
184
 
@@ -178,16 +187,28 @@ export var getDeleteMechanism = function getDeleteMechanism(tr, state) {
178
187
  if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
179
188
  return 'deleteButton';
180
189
  }
190
+ var hasStructuralStep = isSyncBlockActivationEnabled && tr.steps.some(function (step) {
191
+ return step instanceof ReplaceAroundStep;
192
+ });
181
193
  var hasReplaceStep = tr.steps.some(function (step) {
182
194
  return step instanceof ReplaceStep;
183
195
  });
184
- if (!hasReplaceStep) {
196
+ if (hasStructuralStep || !hasReplaceStep) {
185
197
  return 'other';
186
198
  }
187
199
  var selection = state.selection;
188
- var isNodeSelected = selection instanceof NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
200
+ var isNodeSelected = selection instanceof NodeSelection && (((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock' || isSyncBlockActivationEnabled && ((_selection$node2 = selection.node) === null || _selection$node2 === void 0 ? void 0 : _selection$node2.type.name) === 'syncBlock');
189
201
  return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
190
202
  };
203
+ export var getPromptedFeedbackEntryPoint = function getPromptedFeedbackEntryPoint(mechanism) {
204
+ if (mechanism === 'undo') {
205
+ return 'prompted-undo';
206
+ }
207
+ if (mechanism === 'deleteButton' || mechanism === 'keyboardDelete' || mechanism === 'selectionReplaced') {
208
+ return 'prompted-delete';
209
+ }
210
+ return undefined;
211
+ };
191
212
  /**
192
213
  * True when a source `bodiedSyncBlock` holds user content — i.e. not just the
193
214
  * single empty paragraph it is created with (used to detect empty→content).
@@ -232,6 +253,7 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
232
253
  syncBlockStore = _ref3.syncBlockStore,
233
254
  api = _ref3.api,
234
255
  confirmationTransactionRef = _ref3.confirmationTransactionRef,
256
+ ctx = _ref3.ctx,
235
257
  bodiedSyncBlockRemoved = _ref3.bodiedSyncBlockRemoved,
236
258
  bodiedSyncBlockAdded = _ref3.bodiedSyncBlockAdded,
237
259
  extensionFlagShown = _ref3.extensionFlagShown;
@@ -259,6 +281,19 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
259
281
  // dedicated `cross_product_paste` event subject.
260
282
  var isPaste = Boolean((_tr$getMeta = tr.getMeta('paste')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : tr.getMeta('uiEvent') === 'paste');
261
283
  var isPasteOrDrop = isPaste || tr.getMeta('uiEvent') === 'drop';
284
+ var hasRemovedSyncBlock = syncBlockRemoved.length > 0 || bodiedSyncBlockRemoved.length > 0;
285
+ var isCutRemoval = hasRemovedSyncBlock && ctx.consumeCutRemoval();
286
+ var canPromptForRemoval = Boolean(ctx.onGiveFeedback) && hasRemovedSyncBlock && !isDirtyTransaction(tr) && !isCutRemoval && !isPasteOrDrop;
287
+ var promptedFeedbackEntryPoint = canPromptForRemoval ? getPromptedFeedbackEntryPoint(getDeleteMechanism(tr, state)) : undefined;
288
+ var entryPoint = promptedFeedbackEntryPoint && expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true) ? promptedFeedbackEntryPoint : undefined;
289
+ if (entryPoint && syncBlockRemoved.length > 0 && bodiedSyncBlockRemoved.length === 0) {
290
+ tr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
291
+ context: {
292
+ blockType: 'reference',
293
+ entryPoint: entryPoint
294
+ }
295
+ });
296
+ }
262
297
  syncBlockAdded.forEach(function (syncBlock) {
263
298
  var _api$analytics2;
264
299
  api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || (_api$analytics2 = _api$analytics2.actions) === null || _api$analytics2 === void 0 || _api$analytics2.fireAnalyticsEvent({
@@ -286,7 +321,28 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
286
321
  }
287
322
  // Only derive the mechanism behind the gate — discarded gate-off.
288
323
  var mechanism = fg('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
289
- return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
324
+ var feedbackContext = entryPoint && getDeleteReason(tr) !== 'source-block-unsynced' ? {
325
+ blockType: 'source',
326
+ entryPoint: entryPoint
327
+ } : undefined;
328
+ if (feedbackContext) {
329
+ var sourceAttempt = ctx.queueSourceFeedback(feedbackContext);
330
+ return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, {
331
+ onDeleteCompleted: function onDeleteCompleted(success) {
332
+ return ctx.completeSourceDeletion(sourceAttempt, success);
333
+ },
334
+ onDeleteTransaction: function onDeleteTransaction(deleteTr) {
335
+ return deleteTr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
336
+ context: feedbackContext,
337
+ sourceAttempt: sourceAttempt
338
+ });
339
+ },
340
+ onDestroy: function onDestroy() {
341
+ return ctx.clearSourceFeedback(sourceAttempt);
342
+ }
343
+ });
344
+ }
345
+ return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, undefined);
290
346
  }
291
347
  if (bodiedSyncBlockAdded.length > 0) {
292
348
  if (tr.getMeta(pmHistoryPluginKey)) {
@@ -408,15 +464,18 @@ var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore
408
464
  * is explicitly scoped to a single plugin instance.
409
465
  */
410
466
  var SyncedBlockPluginContext = /*#__PURE__*/function () {
411
- function SyncedBlockPluginContext() {
467
+ function SyncedBlockPluginContext(onGiveFeedback) {
412
468
  _classCallCheck(this, SyncedBlockPluginContext);
413
469
  _defineProperty(this, "confirmationTransactionRef", {
414
470
  current: undefined
415
471
  });
472
+ _defineProperty(this, "_cutRemovalPending", false);
473
+ _defineProperty(this, "cutRemovalGeneration", 0);
416
474
  _defineProperty(this, "_isCopyEvent", false);
417
475
  _defineProperty(this, "_isCutEvent", false);
418
476
  _defineProperty(this, "unpublishedFlagShown", new Set());
419
477
  _defineProperty(this, "extensionFlagShown", new Set());
478
+ this.onGiveFeedback = onGiveFeedback;
420
479
  }
421
480
  return _createClass(SyncedBlockPluginContext, [{
422
481
  key: "isCopyEvent",
@@ -447,19 +506,114 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
447
506
  this._isCutEvent = false;
448
507
  return was;
449
508
  }
509
+ }, {
510
+ key: "markCutRemovalPending",
511
+ value: function markCutRemovalPending() {
512
+ var _this = this;
513
+ this._cutRemovalPending = true;
514
+ var generation = ++this.cutRemovalGeneration;
515
+ queueMicrotask(function () {
516
+ if (_this.cutRemovalGeneration === generation) {
517
+ _this._cutRemovalPending = false;
518
+ }
519
+ });
520
+ }
521
+ }, {
522
+ key: "consumeCutRemoval",
523
+ value: function consumeCutRemoval() {
524
+ var wasPending = this._cutRemovalPending;
525
+ this._cutRemovalPending = false;
526
+ this.cutRemovalGeneration += 1;
527
+ return wasPending;
528
+ }
529
+ }, {
530
+ key: "queueSourceFeedback",
531
+ value: function queueSourceFeedback(context) {
532
+ var sourceAttempt = Symbol('syncedBlockSourceFeedbackAttempt');
533
+ this.pendingSourceFeedback = {
534
+ context: context,
535
+ deletionSucceeded: false,
536
+ sourceAttempt: sourceAttempt,
537
+ transactionApplied: false
538
+ };
539
+ return sourceAttempt;
540
+ }
541
+ }, {
542
+ key: "completeSourceDeletion",
543
+ value: function completeSourceDeletion(sourceAttempt, success) {
544
+ var _this$pendingSourceFe;
545
+ if (((_this$pendingSourceFe = this.pendingSourceFeedback) === null || _this$pendingSourceFe === void 0 ? void 0 : _this$pendingSourceFe.sourceAttempt) !== sourceAttempt) {
546
+ return;
547
+ }
548
+ if (!success) {
549
+ // Keep the attempt pending because retryDeletion reuses these callbacks.
550
+ // Dismissal or cancellation clears it through the destroy callback.
551
+ return;
552
+ }
553
+ this.pendingSourceFeedback.deletionSucceeded = true;
554
+ this.flushSourceFeedback(sourceAttempt);
555
+ }
556
+ }, {
557
+ key: "clearSourceFeedback",
558
+ value: function clearSourceFeedback(sourceAttempt) {
559
+ var _this$pendingSourceFe2;
560
+ if (((_this$pendingSourceFe2 = this.pendingSourceFeedback) === null || _this$pendingSourceFe2 === void 0 ? void 0 : _this$pendingSourceFe2.sourceAttempt) === sourceAttempt) {
561
+ this.pendingSourceFeedback = undefined;
562
+ }
563
+ }
564
+ }, {
565
+ key: "handleAppliedFeedback",
566
+ value: function handleAppliedFeedback(_ref6) {
567
+ var _this$pendingSourceFe3;
568
+ var context = _ref6.context,
569
+ sourceAttempt = _ref6.sourceAttempt;
570
+ if (context.blockType === 'source' && sourceAttempt && ((_this$pendingSourceFe3 = this.pendingSourceFeedback) === null || _this$pendingSourceFe3 === void 0 ? void 0 : _this$pendingSourceFe3.sourceAttempt) === sourceAttempt) {
571
+ this.pendingSourceFeedback.transactionApplied = true;
572
+ this.flushSourceFeedback(sourceAttempt);
573
+ return;
574
+ }
575
+ if (context.blockType === 'reference') {
576
+ this.invokeFeedback(context);
577
+ }
578
+ }
579
+ }, {
580
+ key: "flushSourceFeedback",
581
+ value: function flushSourceFeedback(sourceAttempt) {
582
+ var _this$pendingSourceFe4, _this$pendingSourceFe5;
583
+ if (((_this$pendingSourceFe4 = this.pendingSourceFeedback) === null || _this$pendingSourceFe4 === void 0 ? void 0 : _this$pendingSourceFe4.sourceAttempt) === sourceAttempt && (_this$pendingSourceFe5 = this.pendingSourceFeedback) !== null && _this$pendingSourceFe5 !== void 0 && _this$pendingSourceFe5.deletionSucceeded && this.pendingSourceFeedback.transactionApplied) {
584
+ var context = this.pendingSourceFeedback.context;
585
+ this.pendingSourceFeedback = undefined;
586
+ this.invokeFeedback(context);
587
+ }
588
+ }
589
+ }, {
590
+ key: "invokeFeedback",
591
+ value: function invokeFeedback(context) {
592
+ var _this2 = this;
593
+ deferDispatch(function () {
594
+ try {
595
+ var _this2$onGiveFeedback;
596
+ var feedbackResult = (_this2$onGiveFeedback = _this2.onGiveFeedback) === null || _this2$onGiveFeedback === void 0 ? void 0 : _this2$onGiveFeedback.call(_this2, context);
597
+ void Promise.resolve(feedbackResult).catch(function () {});
598
+ } catch (_unused) {
599
+ // Product feedback collectors are optional UI. Their failures must
600
+ // never affect the already-applied editor transaction.
601
+ }
602
+ });
603
+ }
450
604
  }]);
451
605
  }();
452
606
  export var createPlugin = function createPlugin(options, pmPluginFactoryParams, syncBlockStore, api) {
453
- var _ref6 = options || {},
454
- _ref6$useLongPressSel = _ref6.useLongPressSelection,
455
- useLongPressSelection = _ref6$useLongPressSel === void 0 ? false : _ref6$useLongPressSel;
607
+ var _ref7 = options || {},
608
+ _ref7$useLongPressSel = _ref7.useLongPressSelection,
609
+ useLongPressSelection = _ref7$useLongPressSel === void 0 ? false : _ref7$useLongPressSel;
456
610
 
457
611
  // Cache the experiment value once at plugin creation time.
458
612
  // This fires the exposure event exactly once (correct per Exposure Events 101)
459
613
  // and avoids ~10 redundant Statsig SDK evaluations per keystroke in hot paths
460
614
  // (apply, filterTransaction, appendTransaction, decorations).
461
615
  var isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
462
- var ctx = new SyncedBlockPluginContext();
616
+ var ctx = new SyncedBlockPluginContext(options === null || options === void 0 ? void 0 : options.onGiveFeedback);
463
617
  var confirmationTransactionRef = ctx.confirmationTransactionRef;
464
618
  var unpublishedFlagShown = ctx.unpublishedFlagShown;
465
619
  var extensionFlagShown = ctx.extensionFlagShown;
@@ -469,8 +623,8 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
469
623
  // Classic pages with sync blocks.
470
624
  syncBlockStore.sourceManager.registerFlushCompletionCallback(function () {
471
625
  deferDispatch(function () {
472
- api === null || api === void 0 || api.core.actions.execute(function (_ref7) {
473
- var tr = _ref7.tr;
626
+ api === null || api === void 0 || api.core.actions.execute(function (_ref8) {
627
+ var tr = _ref8.tr;
474
628
  return tr;
475
629
  });
476
630
  });
@@ -498,8 +652,8 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
498
652
  // case the Confluence default is used.
499
653
  var sourceProduct = getSourceProductFromResourceIdSafe(resourceId);
500
654
  deferDispatch(function () {
501
- api === null || api === void 0 || api.core.actions.execute(function (_ref8) {
502
- var tr = _ref8.tr;
655
+ api === null || api === void 0 || api.core.actions.execute(function (_ref9) {
656
+ var tr = _ref9.tr;
503
657
  return tr.setMeta(syncedBlockPluginKey, {
504
658
  activeFlag: {
505
659
  id: FLAG_ID.UNPUBLISHED_SYNC_BLOCK_PASTED,
@@ -811,7 +965,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
811
965
  return false;
812
966
  },
813
967
  cut: function cut() {
814
- if (fg('platform_synced_block_patch_13')) {
968
+ if (fg('platform_synced_block_patch_13') || Boolean(options === null || options === void 0 ? void 0 : options.onGiveFeedback) && expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true)) {
815
969
  ctx.consumeCutEvent();
816
970
  ctx.markCutEvent();
817
971
  }
@@ -842,27 +996,47 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
842
996
  return node;
843
997
  });
844
998
  },
845
- transformCopied: function transformCopied(slice, _ref9) {
846
- var state = _ref9.state;
999
+ transformCopied: function transformCopied(slice, _ref0) {
1000
+ var state = _ref0.state;
847
1001
  var pluginState = syncedBlockPluginKey.getState(state);
848
1002
  var syncBlockStore = pluginState === null || pluginState === void 0 ? void 0 : pluginState.syncBlockStore;
849
1003
  var schema = state.schema;
850
1004
  var isCopy = ctx.consumeCopyEvent();
851
1005
  var isSyncedBlockPatch13Enabled = fg('platform_synced_block_patch_13');
852
- var isCut = isSyncedBlockPatch13Enabled && ctx.consumeCutEvent();
853
- if (!syncBlockStore || !isCopy && !isCut) {
1006
+ var wasCut = ctx.consumeCutEvent();
1007
+ var isCut = isSyncedBlockPatch13Enabled && wasCut;
1008
+ var isPromptedFeedbackCut = Boolean(syncBlockStore && (options === null || options === void 0 ? void 0 : options.onGiveFeedback) && wasCut) && expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true);
1009
+ if (!syncBlockStore || !isCopy && !isCut && !isPromptedFeedbackCut) {
854
1010
  return slice;
855
1011
  }
856
- return mapSlice(slice, function (node) {
1012
+ if (isPromptedFeedbackCut && !isCut) {
1013
+ var _containsCutSyncBlock = false;
1014
+ mapSlice(slice, function (node) {
1015
+ if (syncBlockStore.referenceManager.isReferenceBlock(node) || node.type.name === 'bodiedSyncBlock' && sliceFullyContainsNode(slice, node)) {
1016
+ _containsCutSyncBlock = true;
1017
+ }
1018
+ return node;
1019
+ });
1020
+ if (_containsCutSyncBlock) {
1021
+ ctx.markCutRemovalPending();
1022
+ }
1023
+ return slice;
1024
+ }
1025
+ var containsCutSyncBlock = false;
1026
+ var transformedSlice = mapSlice(slice, function (node) {
857
1027
  if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
858
1028
  var _syncBlockStore$refer;
859
1029
  if (isCut) {
1030
+ containsCutSyncBlock = isPromptedFeedbackCut;
860
1031
  return node;
861
1032
  }
862
1033
  showCopiedFlag(api, options === null || options === void 0 ? void 0 : options.__livePage, ((_syncBlockStore$refer = syncBlockStore.referenceManager.getFromCache(node.attrs.resourceId)) === null || _syncBlockStore$refer === void 0 || (_syncBlockStore$refer = _syncBlockStore$refer.data) === null || _syncBlockStore$refer === void 0 ? void 0 : _syncBlockStore$refer.status) === 'unpublished', getSourceProductFromResourceIdSafe(node.attrs.resourceId));
863
1034
  return node;
864
1035
  }
865
1036
  if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
1037
+ if (isPromptedFeedbackCut && isCut && sliceFullyContainsNode(slice, node)) {
1038
+ containsCutSyncBlock = true;
1039
+ }
866
1040
  // if we only selected part of the bodied sync block content,
867
1041
  // remove the sync block node and only keep the content
868
1042
  if (!sliceFullyContainsNode(slice, node)) {
@@ -886,6 +1060,10 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
886
1060
  }
887
1061
  return node;
888
1062
  });
1063
+ if (containsCutSyncBlock) {
1064
+ ctx.markCutRemovalPending();
1065
+ }
1066
+ return transformedSlice;
889
1067
  }
890
1068
  },
891
1069
  filterTransaction: function filterTransaction(tr, state) {
@@ -972,6 +1150,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
972
1150
  syncBlockStore: syncBlockStore,
973
1151
  api: api,
974
1152
  confirmationTransactionRef: confirmationTransactionRef,
1153
+ ctx: ctx,
975
1154
  bodiedSyncBlockRemoved: bodiedSyncBlockRemoved,
976
1155
  bodiedSyncBlockAdded: bodiedSyncBlockAdded,
977
1156
  extensionFlagShown: extensionFlagShown
@@ -995,6 +1174,17 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
995
1174
  if (viewMode === 'view') {
996
1175
  return null;
997
1176
  }
1177
+ if (options !== null && options !== void 0 && options.onGiveFeedback) {
1178
+ var feedbackContexts = trs.flatMap(function (tr) {
1179
+ var feedbackContext = tr.getMeta(syncedBlockPromptedFeedbackMetaKey);
1180
+ return feedbackContext ? [feedbackContext] : [];
1181
+ });
1182
+ if (feedbackContexts.length > 0 && expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true)) {
1183
+ feedbackContexts.forEach(function (feedbackContext) {
1184
+ ctx.handleAppliedFeedback(feedbackContext);
1185
+ });
1186
+ }
1187
+ }
998
1188
 
999
1189
  // Update source sync block cache for user-initiated changes only.
1000
1190
  // Cache updates are handled here instead of in the nodeview update() so we
@@ -1079,8 +1269,8 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
1079
1269
  if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep) || !('slice' in step)) {
1080
1270
  return false;
1081
1271
  }
1082
- var _ref0 = step,
1083
- slice = _ref0.slice;
1272
+ var _ref1 = step,
1273
+ slice = _ref1.slice;
1084
1274
  var found = false;
1085
1275
  slice.content.descendants(function (node) {
1086
1276
  if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
@@ -1120,8 +1310,8 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
1120
1310
  tr.setMeta('addToHistory', false);
1121
1311
  deferDispatch(function () {
1122
1312
  var _api$core;
1123
- api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref1) {
1124
- var tr = _ref1.tr;
1313
+ api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref10) {
1314
+ var tr = _ref10.tr;
1125
1315
  return tr.setMeta(syncedBlockPluginKey, {
1126
1316
  activeFlag: {
1127
1317
  id: FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK
@@ -5,7 +5,7 @@ import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
5
5
  import { fg } from '@atlaskit/platform-feature-flags';
6
6
  import { FLAG_ID } from '../../types';
7
7
  import { syncedBlockPluginKey } from '../main';
8
- import { recomputeDeleteTransaction } from './recompute-delete-transaction';
8
+ import { 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,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
27
27
  });
28
28
  };
29
29
  };
30
- export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
30
+ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism, feedbackCallbacks) {
31
31
  // Clear potential old pending deletion to retreat the deletion as first attempt
32
32
  syncBlockStore.sourceManager.clearPendingDeletion();
33
33
 
@@ -49,7 +49,15 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
49
49
  // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
50
50
  api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
51
51
  var tr = _ref2.tr;
52
- var recomputedTr = recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
52
+ // EDITOR-8230: unsync must remove only the sync wrapper and keep the block's
53
+ // content inline, whereas delete removes the whole block. Before this branch,
54
+ // the confirm path always recomputed a full delete, so unsyncing a source block
55
+ // silently deleted its content. When the deletion was actually triggered by
56
+ // unsync, recompute an unwrap (replaceWith content) instead of a delete.
57
+ var isUnsync = fg('platform_editor_blocks_patch_6') && deletionReason === 'source-block-unsynced';
58
+ var recomputedTr = isUnsync ? recomputeUnsyncTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
59
+ return node.attrs;
60
+ })) : recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
53
61
  return node.attrs;
54
62
  }));
55
63
 
@@ -63,6 +71,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
63
71
  return null;
64
72
  }
65
73
  recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
74
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(recomputedTr);
66
75
  if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
67
76
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
68
77
  // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
@@ -78,6 +87,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
78
87
  }
79
88
  api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function () {
80
89
  var trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
90
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(trToDispatch);
81
91
  if (!trToDispatch.getMeta(pmHistoryPluginKey)) {
82
92
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
83
93
  // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
@@ -87,6 +97,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
87
97
  });
88
98
  }, function (success) {
89
99
  var _api$core4;
100
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteCompleted(success);
90
101
  api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
91
102
  var tr = _ref3.tr;
92
103
  var newState;
@@ -114,6 +125,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
114
125
  });
115
126
  }, function () {
116
127
  confirmationTransactionRef.current = undefined;
128
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDestroy();
117
129
  }, mechanism);
118
130
  return false;
119
131
  };
@@ -1,3 +1,7 @@
1
+ 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; } } }; }
2
+ 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; } }
3
+ 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; }
4
+ import { Slice } from '@atlaskit/editor-prosemirror/model';
1
5
  /**
2
6
  * Whether `node` is the source block requested by `wanted`.
3
7
  *
@@ -89,4 +93,65 @@ export var recomputeDeleteTransaction = function recomputeDeleteTransaction(tr,
89
93
  tr.delete(pos, pos + node.nodeSize);
90
94
  });
91
95
  return tr;
96
+ };
97
+
98
+ /**
99
+ * Recompute a source `bodiedSyncBlock` *unsync* from the live document.
100
+ *
101
+ * Unsync differs from delete: the sync wrapper must be removed while its content
102
+ * is preserved inline in the document. Instead of `tr.delete(pos, pos +
103
+ * nodeSize)` (which drops the content too — EDITOR-8230), this replaces each
104
+ * matched source block with its own content.
105
+ *
106
+ * The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content,
107
+ * 0, 0))` — an explicit zero-open slice inserted verbatim — rather than
108
+ * `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's
109
+ * `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing
110
+ * block (e.g. the block's final panel/empty paragraph) when the unwrapped
111
+ * content meets a document boundary, silently losing content (EDITOR-8230). A
112
+ * zero-open `replace` drops the wrapper's own open ends while keeping every child
113
+ * of the fragment as its own top-level node.
114
+ *
115
+ * Like `recomputeDeleteTransaction`, targets are located in the *current*
116
+ * document (`tr.doc`) so the operation is robust to intervening local edits and
117
+ * remote collab changes while the confirmation modal is open, and it handles
118
+ * partial matches gracefully (missing blocks are skipped).
119
+ *
120
+ * Replacements are applied in reverse document order so earlier replacements do
121
+ * not invalidate the positions of later ones.
122
+ *
123
+ * @returns the mutated transaction when at least one target node was found and
124
+ * unwrapped, otherwise `undefined` when none of the targets exist any more.
125
+ */
126
+ export var recomputeUnsyncTransaction = function recomputeUnsyncTransaction(tr, isSourceBlock, syncBlockIds) {
127
+ var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
128
+ if (matches.length === 0) {
129
+ return undefined;
130
+ }
131
+
132
+ // Unwrap in reverse document order so positions remain valid across edits.
133
+ // Use a raw zero-open Slice replace (not `replaceWith`) so the block's content
134
+ // is inserted verbatim as top-level nodes without ProseMirror's replaceRange
135
+ // heuristics collapsing a trailing block (EDITOR-8230).
136
+ var orderedMatches = matches.sort(function (a, b) {
137
+ return b.pos - a.pos;
138
+ });
139
+ var _iterator = _createForOfIteratorHelper(orderedMatches),
140
+ _step;
141
+ try {
142
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
143
+ var _step$value = _step.value,
144
+ node = _step$value.node,
145
+ pos = _step$value.pos;
146
+ // This is ProseMirror's Transaction.replace (a document edit), not String.replace — the
147
+ // perf rule misfires on the method name, and there is nothing to hoist.
148
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
149
+ tr.replace(pos, pos + node.nodeSize, new Slice(node.content, 0, 0));
150
+ }
151
+ } catch (err) {
152
+ _iterator.e(err);
153
+ } finally {
154
+ _iterator.f();
155
+ }
156
+ return tr;
92
157
  };
@@ -30,6 +30,7 @@ export var getToolbarConfig = function getToolbarConfig(state, intl, api, syncBl
30
30
  if (syncBlockStore.sourceManager.isPendingCreation(syncBlockObject.node.attrs.resourceId)) {
31
31
  return;
32
32
  }
33
+ var isSyncBlockActivationEnabled = expValEquals('platform_editor_sync_block_activation', 'isEnabled', true);
33
34
  var syncBlockInstance = syncBlockStore.referenceManager.getFromCache(syncBlockObject.node.attrs.resourceId);
34
35
  var isUnsyncedBlock = (syncBlockInstance === null || syncBlockInstance === void 0 || (_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.type) === SyncBlockError.NotFound;
35
36
  var isErroredBlock = syncBlockInstance === null || syncBlockInstance === void 0 ? void 0 : syncBlockInstance.error;
@@ -50,7 +51,6 @@ export var getToolbarConfig = function getToolbarConfig(state, intl, api, syncBl
50
51
  };
51
52
  };
52
53
  var items = [];
53
- var isSyncBlockActivationEnabled = expValEquals('platform_editor_sync_block_activation', 'isEnabled', true);
54
54
  if (isUnsyncedBlock) {
55
55
  var deleteButton = _objectSpread({
56
56
  type: 'button',
@@ -188,7 +188,8 @@ export var getToolbarConfig = function getToolbarConfig(state, intl, api, syncBl
188
188
  return tr.setSelection(new GapCursorSelection(tr.doc.resolve(syncBlockObject.pos + syncBlockObject.node.nodeSize), Side.RIGHT));
189
189
  });
190
190
  onGiveFeedback({
191
- blockType: isBodiedSyncBlock ? 'source' : 'reference'
191
+ blockType: isBodiedSyncBlock ? 'source' : 'reference',
192
+ entryPoint: 'overflow-menu'
192
193
  });
193
194
  return true;
194
195
  }
@@ -6,7 +6,7 @@ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
6
6
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
7
  import type { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
8
8
  import type { DeletionMechanism } from '@atlaskit/editor-synced-block-provider/common/types';
9
- import type { SyncedBlockPlugin, SyncedBlockPluginOptions } from '../syncedBlockPluginType';
9
+ import type { SyncedBlockFeedbackContext, SyncedBlockPlugin, SyncedBlockPluginOptions } from '../syncedBlockPluginType';
10
10
  import type { ActiveFlag, BodiedSyncBlockDeletionStatus, RetryCreationPosMap } from '../types';
11
11
  export declare const syncedBlockPluginKey: PluginKey;
12
12
  /**
@@ -89,5 +89,6 @@ type SyncedBlockPluginState = {
89
89
  * selected when the edit was made.
90
90
  */
91
91
  export declare const getDeleteMechanism: (tr: Transaction, state: EditorState) => DeletionMechanism;
92
+ export declare const getPromptedFeedbackEntryPoint: (mechanism: DeletionMechanism) => SyncedBlockFeedbackContext['entryPoint'] | undefined;
92
93
  export declare const createPlugin: (options: SyncedBlockPluginOptions | undefined, pmPluginFactoryParams: PMPluginFactoryParams, syncBlockStore: SyncBlockStoreManager, api?: ExtractInjectionAPI<SyncedBlockPlugin>) => SafePlugin<SyncedBlockPluginState>;
93
94
  export {};
@@ -7,4 +7,10 @@ import type { SyncBlockInfo } from '../../types';
7
7
  export type TransactionRef = {
8
8
  current: Transaction | undefined;
9
9
  };
10
- export declare const handleBodiedSyncBlockRemoval: (bodiedSyncBlockRemoved: SyncBlockInfo[], syncBlockStore: SyncBlockStoreManager, api: ExtractInjectionAPI<SyncedBlockPlugin> | undefined, confirmationTransactionRef: TransactionRef, deletionReason: DeletionReason, mechanism?: DeletionMechanism) => boolean;
10
+ type SourceFeedbackCallbacks = {
11
+ onDeleteCompleted: (success: boolean) => void;
12
+ onDeleteTransaction: (tr: Transaction) => void;
13
+ onDestroy: () => void;
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 {};
@@ -27,3 +27,32 @@ import type { SyncBlockAttrs } from '../../types';
27
27
  * collaborator already removed them all — so there is nothing to delete.
28
28
  */
29
29
  export declare const recomputeDeleteTransaction: (tr: Transaction, isSourceBlock: (node: PMNode) => boolean, syncBlockIds: SyncBlockAttrs[]) => Transaction | undefined;
30
+ /**
31
+ * Recompute a source `bodiedSyncBlock` *unsync* from the live document.
32
+ *
33
+ * Unsync differs from delete: the sync wrapper must be removed while its content
34
+ * is preserved inline in the document. Instead of `tr.delete(pos, pos +
35
+ * nodeSize)` (which drops the content too — EDITOR-8230), this replaces each
36
+ * matched source block with its own content.
37
+ *
38
+ * The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content,
39
+ * 0, 0))` — an explicit zero-open slice inserted verbatim — rather than
40
+ * `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's
41
+ * `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing
42
+ * block (e.g. the block's final panel/empty paragraph) when the unwrapped
43
+ * content meets a document boundary, silently losing content (EDITOR-8230). A
44
+ * zero-open `replace` drops the wrapper's own open ends while keeping every child
45
+ * of the fragment as its own top-level node.
46
+ *
47
+ * Like `recomputeDeleteTransaction`, targets are located in the *current*
48
+ * document (`tr.doc`) so the operation is robust to intervening local edits and
49
+ * remote collab changes while the confirmation modal is open, and it handles
50
+ * partial matches gracefully (missing blocks are skipped).
51
+ *
52
+ * Replacements are applied in reverse document order so earlier replacements do
53
+ * not invalidate the positions of later ones.
54
+ *
55
+ * @returns the mutated transaction when at least one target node was found and
56
+ * unwrapped, otherwise `undefined` when none of the targets exist any more.
57
+ */
58
+ export declare const recomputeUnsyncTransaction: (tr: Transaction, isSourceBlock: (node: PMNode) => boolean, syncBlockIds: SyncBlockAttrs[]) => Transaction | undefined;
@@ -45,7 +45,7 @@ export type SyncedBlockRendererProps = {
45
45
  };
46
46
  export type SyncedBlockFeedbackContext = {
47
47
  blockType: 'source' | 'reference';
48
- entryPoint?: string;
48
+ entryPoint: 'overflow-menu' | 'prompted-delete' | 'prompted-undo';
49
49
  };
50
50
  export interface SyncedBlockPluginOptions extends LongPressSelectionPluginOptions {
51
51
  /**
@@ -62,7 +62,7 @@ export interface SyncedBlockPluginOptions extends LongPressSelectionPluginOption
62
62
  * The editor plugin owns the menu affordance; the product owns the collector
63
63
  * configuration and lifecycle.
64
64
  */
65
- onGiveFeedback?: (context: SyncedBlockFeedbackContext) => void;
65
+ onGiveFeedback?: (context: SyncedBlockFeedbackContext) => Promise<void> | void;
66
66
  syncBlockDataProvider: SyncBlockDataProviderInterface;
67
67
  syncedBlockRenderer: (props: SyncedBlockRendererProps) => React.JSX.Element;
68
68
  }