@atlaskit/editor-plugin-synced-block 10.4.6 → 10.5.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
@@ -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
 
@@ -63,6 +63,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
63
63
  return null;
64
64
  }
65
65
  recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
66
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(recomputedTr);
66
67
  if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
67
68
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
68
69
  // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
@@ -78,6 +79,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
78
79
  }
79
80
  api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function () {
80
81
  var trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
82
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(trToDispatch);
81
83
  if (!trToDispatch.getMeta(pmHistoryPluginKey)) {
82
84
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
83
85
  // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
@@ -87,6 +89,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
87
89
  });
88
90
  }, function (success) {
89
91
  var _api$core4;
92
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteCompleted(success);
90
93
  api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
91
94
  var tr = _ref3.tr;
92
95
  var newState;
@@ -114,6 +117,7 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
114
117
  });
115
118
  }, function () {
116
119
  confirmationTransactionRef.current = undefined;
120
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDestroy();
117
121
  }, mechanism);
118
122
  return false;
119
123
  };
@@ -14,6 +14,7 @@ import { DeleteConfirmationModal } from './ui/DeleteConfirmationModal';
14
14
  import { Flag } from './ui/Flag';
15
15
  import { getToolbarConfig } from './ui/floating-toolbar';
16
16
  import { getQuickInsertConfig } from './ui/quick-insert';
17
+ import { SourceSyncBlockPlaceholder } from './ui/SourceSyncBlockPlaceholder';
17
18
  import { SyncBlockRefresher } from './ui/SyncBlockRefresher';
18
19
  import { getToolbarComponents } from './ui/toolbar-components';
19
20
 
@@ -151,10 +152,10 @@ export var syncedBlockPlugin = function syncedBlockPlugin(_ref2) {
151
152
  refs.containerElement = containerElement || undefined;
152
153
  refs.popupsMountPoint = popupsMountPoint || undefined;
153
154
  refs.wrapperElement = wrapperElement || undefined;
154
- return /*#__PURE__*/React.createElement(LazySyncedBlockUI, {
155
+ return /*#__PURE__*/React.createElement(React.Fragment, null, expValEquals('platform_editor_sync_block_activation', 'isEnabled', true) && /*#__PURE__*/React.createElement(SourceSyncBlockPlaceholder, null), /*#__PURE__*/React.createElement(LazySyncedBlockUI, {
155
156
  syncBlockStore: syncBlockStore,
156
157
  api: api
157
- });
158
+ }));
158
159
  },
159
160
  getSharedState: function getSharedState(editorState) {
160
161
  if (!editorState) {
@@ -0,0 +1,42 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ /**
3
+ * @jsxRuntime classic
4
+ * @jsx jsx
5
+ */
6
+ import { useMemo } from 'react';
7
+
8
+ // Plugin content components need a global selector because the ProseMirror content is not a child
9
+ // of this component. This follows the existing layout placeholder pattern.
10
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles, @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
11
+ import { css, Global, jsx } from '@emotion/react';
12
+ import { useIntl } from 'react-intl';
13
+ import { placeholderTextMessages } from '@atlaskit/editor-common/messages';
14
+ import { BodiedSyncBlockSharedCssClassName } from '@atlaskit/editor-common/sync-block';
15
+ var getSourceSyncBlockPlaceholderStyles = function getSourceSyncBlockPlaceholderStyles(placeholderText) {
16
+ return css(_defineProperty({}, ".ProseMirror .".concat(BodiedSyncBlockSharedCssClassName.prefix, " .").concat(BodiedSyncBlockSharedCssClassName.content, "[contenteditable=\"true\"] > p:only-child:has(.ProseMirror-trailingBreak:only-child)"), {
17
+ position: 'relative',
18
+ '&::before': {
19
+ // JSON.stringify produces a quoted and escaped CSS string.
20
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values
21
+ content: placeholderText,
22
+ position: 'absolute',
23
+ boxSizing: 'border-box',
24
+ color: "var(--ds-text-subtlest, #6B6E76)",
25
+ font: 'inherit',
26
+ insetInlineStart: 0,
27
+ width: '100%',
28
+ pointerEvents: 'none',
29
+ userSelect: 'none'
30
+ }
31
+ }));
32
+ };
33
+ export var SourceSyncBlockPlaceholder = function SourceSyncBlockPlaceholder() {
34
+ var _useIntl = useIntl(),
35
+ formatMessage = _useIntl.formatMessage;
36
+ var placeholderStyles = useMemo(function () {
37
+ return getSourceSyncBlockPlaceholderStyles(JSON.stringify(formatMessage(placeholderTextMessages.sourceSyncBlockPlaceholderText)));
38
+ }, [formatMessage]);
39
+ return jsx(Global, {
40
+ styles: placeholderStyles
41
+ });
42
+ };
@@ -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 {};
@@ -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
  }
@@ -0,0 +1 @@
1
+ export declare const SourceSyncBlockPlaceholder: () => JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "10.4.6",
3
+ "version": "10.5.0",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -46,18 +46,19 @@
46
46
  "@atlaskit/platform-feature-flags": "^2.1.0",
47
47
  "@atlaskit/primitives": "^22.2.0",
48
48
  "@atlaskit/spinner": "20.1.1",
49
- "@atlaskit/tmp-editor-statsig": "^135.0.0",
49
+ "@atlaskit/tmp-editor-statsig": "^135.3.0",
50
50
  "@atlaskit/tokens": "16.3.0",
51
51
  "@atlaskit/tooltip": "^23.2.0",
52
52
  "@atlaskit/visually-hidden": "^4.2.0",
53
53
  "@babel/runtime": "^7.0.0",
54
54
  "@compiled/react": "^1.0.0",
55
+ "@emotion/react": "^11.7.1",
55
56
  "bind-event-listener": "^3.0.0",
56
57
  "date-fns": "^2.17.0",
57
58
  "memoize-one": "^6.0.0"
58
59
  },
59
60
  "peerDependencies": {
60
- "@atlaskit/editor-common": "^116.41.0",
61
+ "@atlaskit/editor-common": "^116.44.0",
61
62
  "react": "^18.2.0",
62
63
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
63
64
  },