@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.
package/AGENTS.md CHANGED
@@ -98,8 +98,8 @@ Behind the `editor_synced_block_perf` experiment, `main.ts`:
98
98
  2. Calls `editor-commands/createSyncedBlock` → inserts `bodiedSyncBlock` node into document (marked
99
99
  as **pending creation** — it is not persisted yet)
100
100
  3. `pm-plugins/main.ts` detects new node → `handle-bodied-sync-block-creation.ts` updates
101
- `sourceManager` state. Persistence happens later when the product layer calls `flush()`
102
- (via `flushBodiedSyncBlocks`), which creates the block in the Block Service and then reconciles
101
+ `sourceManager` state. Persistence happens later when the product layer calls `flush()` (via
102
+ `flushBodiedSyncBlocks`), which creates the block in the Block Service and then reconciles
103
103
  identifiers via `commitPendingCreation()`
104
104
  4. `menu-and-toolbar-experiences.ts` fires the experience event
105
105
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 10.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`564443b22eeeb`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/564443b22eeeb) -
8
+ Fix source synced block unsync deleting the entire block and its content instead of unwrapping it
9
+ when the cursor is inside the block (behind `platform_editor_blocks_patch_6`).
10
+
11
+ This release also adds a new `./types` subpath export so consumers can import the `SyncBlockAttrs`
12
+ type without reaching into the package internals:
13
+
14
+ ```ts
15
+ import type { SyncBlockAttrs } from '@atlaskit/editor-plugin-synced-block/types';
16
+ ```
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+
22
+ ## 10.5.0
23
+
24
+ ### Minor Changes
25
+
26
+ - [`3d4d7717366f7`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/3d4d7717366f7) -
27
+ Prompt for feedback after eligible synced block deletion or undo
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies
32
+
3
33
  ## 10.4.7
4
34
 
5
35
  ### Patch Changes
@@ -14,6 +14,7 @@ var _state = require("@atlaskit/editor-prosemirror/state");
14
14
  var _utils = require("@atlaskit/editor-prosemirror/utils");
15
15
  var _utils2 = require("@atlaskit/editor-synced-block-provider/utils");
16
16
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
17
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
17
18
  var _main = require("../pm-plugins/main");
18
19
  var _utils3 = require("../pm-plugins/utils/utils");
19
20
  var _types = require("../types");
@@ -355,7 +356,11 @@ var removeSyncedBlockAtPos = exports.removeSyncedBlockAtPos = function removeSyn
355
356
  var node = tr.doc.nodeAt(pos);
356
357
  if ((node === null || node === void 0 ? void 0 : node.type.name) === 'syncBlock') {
357
358
  var _node$nodeSize;
358
- return tr.replace(pos, pos + ((_node$nodeSize = node === null || node === void 0 ? void 0 : node.nodeSize) !== null && _node$nodeSize !== void 0 ? _node$nodeSize : 0));
359
+ var removeTr = tr.replace(pos, pos + ((_node$nodeSize = node === null || node === void 0 ? void 0 : node.nodeSize) !== null && _node$nodeSize !== void 0 ? _node$nodeSize : 0));
360
+ if ((0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true)) {
361
+ removeTr.setMeta(_main.deleteMechanismMetaKey, 'deleteButton');
362
+ }
363
+ return removeTr;
359
364
  }
360
365
  return tr;
361
366
  });
@@ -374,6 +379,12 @@ var unsync = exports.unsync = function unsync(storeManager, isBodiedSyncBlock, v
374
379
  return false;
375
380
  }
376
381
  if (isBodiedSyncBlock) {
382
+ // Signal the unsync intent. This transaction is intercepted by the plugin's
383
+ // filterTransaction, which shows the deletion-confirmation modal and, on confirm,
384
+ // recomputes the actual document change from the live document keyed off the
385
+ // `deletionReason: 'source-block-unsynced'` meta (see handleBodiedSyncBlockRemoval /
386
+ // recomputeUnsyncTransaction). The real unwrap-vs-delete decision therefore lives in the
387
+ // confirm path, not here (EDITOR-8230).
377
388
  var content = syncBlock === null || syncBlock === void 0 ? void 0 : syncBlock.node.content;
378
389
  var tr = state.tr;
379
390
  tr.replaceWith(syncBlock.pos, syncBlock.pos + syncBlock.node.nodeSize, content).setMeta('deletionReason', 'source-block-unsynced');
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.syncedBlockPluginKey = exports.getDeleteMechanism = exports.deleteMechanismMetaKey = exports.creationMetaKey = exports.createPlugin = void 0;
7
+ exports.syncedBlockPluginKey = exports.getPromptedFeedbackEntryPoint = exports.getDeleteMechanism = exports.deleteMechanismMetaKey = exports.creationMetaKey = exports.createPlugin = void 0;
8
8
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
@@ -24,6 +24,7 @@ var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider
24
24
  var _utils2 = require("@atlaskit/editor-synced-block-provider/utils");
25
25
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
26
26
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
27
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
27
28
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
28
29
  var _bodiedSyncedBlock = require("../nodeviews/bodiedSyncedBlock");
29
30
  var _syncedBlock = require("../nodeviews/syncedBlock");
@@ -53,6 +54,7 @@ var syncedBlockPluginKey = exports.syncedBlockPluginKey = new _state.PluginKey('
53
54
  * `platform_editor_blocks_patch_4`.
54
55
  */
55
56
  var deleteMechanismMetaKey = exports.deleteMechanismMetaKey = new _state.PluginKey('syncedBlockDeleteMechanism');
57
+ var syncedBlockPromptedFeedbackMetaKey = new _state.PluginKey('syncedBlockPromptedFeedback');
56
58
 
57
59
  /**
58
60
  * Creation-type signals set by {@link createSyncedBlock} on the creating
@@ -149,7 +151,7 @@ var getDeleteReason = function getDeleteReason(tr) {
149
151
 
150
152
  /** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
151
153
  var isHistoryMeta = function isHistoryMeta(meta) {
152
- return (0, _typeof2.default)(meta) === 'object' && meta !== null && 'redo' in meta;
154
+ return (0, _typeof2.default)(meta) === 'object' && meta !== null && 'redo' in meta && typeof meta.redo === 'boolean';
153
155
  };
154
156
 
155
157
  /**
@@ -174,9 +176,16 @@ var isHistoryMeta = function isHistoryMeta(meta) {
174
176
  * selected when the edit was made.
175
177
  */
176
178
  var getDeleteMechanism = exports.getDeleteMechanism = function getDeleteMechanism(tr, state) {
177
- var _selection$node;
179
+ var _selection$node, _selection$node2;
180
+ // Keep the legacy analytics classification unchanged while activation is off.
181
+ // The stricter history, structural-step, and reference-selection handling is
182
+ // only needed by the prompted feedback rollout.
183
+ var isSyncBlockActivationEnabled = (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true);
178
184
  var historyMeta = tr.getMeta(_utils.pmHistoryPluginKey);
179
185
  if (historyMeta) {
186
+ if (isSyncBlockActivationEnabled && !isHistoryMeta(historyMeta)) {
187
+ return 'other';
188
+ }
180
189
  return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
181
190
  }
182
191
 
@@ -185,16 +194,28 @@ var getDeleteMechanism = exports.getDeleteMechanism = function getDeleteMechanis
185
194
  if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
186
195
  return 'deleteButton';
187
196
  }
197
+ var hasStructuralStep = isSyncBlockActivationEnabled && tr.steps.some(function (step) {
198
+ return step instanceof _transform.ReplaceAroundStep;
199
+ });
188
200
  var hasReplaceStep = tr.steps.some(function (step) {
189
201
  return step instanceof _transform.ReplaceStep;
190
202
  });
191
- if (!hasReplaceStep) {
203
+ if (hasStructuralStep || !hasReplaceStep) {
192
204
  return 'other';
193
205
  }
194
206
  var selection = state.selection;
195
- var isNodeSelected = selection instanceof _state.NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
207
+ var isNodeSelected = selection instanceof _state.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');
196
208
  return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
197
209
  };
210
+ var getPromptedFeedbackEntryPoint = exports.getPromptedFeedbackEntryPoint = function getPromptedFeedbackEntryPoint(mechanism) {
211
+ if (mechanism === 'undo') {
212
+ return 'prompted-undo';
213
+ }
214
+ if (mechanism === 'deleteButton' || mechanism === 'keyboardDelete' || mechanism === 'selectionReplaced') {
215
+ return 'prompted-delete';
216
+ }
217
+ return undefined;
218
+ };
198
219
  /**
199
220
  * True when a source `bodiedSyncBlock` holds user content — i.e. not just the
200
221
  * single empty paragraph it is created with (used to detect empty→content).
@@ -239,6 +260,7 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
239
260
  syncBlockStore = _ref3.syncBlockStore,
240
261
  api = _ref3.api,
241
262
  confirmationTransactionRef = _ref3.confirmationTransactionRef,
263
+ ctx = _ref3.ctx,
242
264
  bodiedSyncBlockRemoved = _ref3.bodiedSyncBlockRemoved,
243
265
  bodiedSyncBlockAdded = _ref3.bodiedSyncBlockAdded,
244
266
  extensionFlagShown = _ref3.extensionFlagShown;
@@ -266,6 +288,19 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
266
288
  // dedicated `cross_product_paste` event subject.
267
289
  var isPaste = Boolean((_tr$getMeta = tr.getMeta('paste')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : tr.getMeta('uiEvent') === 'paste');
268
290
  var isPasteOrDrop = isPaste || tr.getMeta('uiEvent') === 'drop';
291
+ var hasRemovedSyncBlock = syncBlockRemoved.length > 0 || bodiedSyncBlockRemoved.length > 0;
292
+ var isCutRemoval = hasRemovedSyncBlock && ctx.consumeCutRemoval();
293
+ var canPromptForRemoval = Boolean(ctx.onGiveFeedback) && hasRemovedSyncBlock && !(0, _collab.isDirtyTransaction)(tr) && !isCutRemoval && !isPasteOrDrop;
294
+ var promptedFeedbackEntryPoint = canPromptForRemoval ? getPromptedFeedbackEntryPoint(getDeleteMechanism(tr, state)) : undefined;
295
+ var entryPoint = promptedFeedbackEntryPoint && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true) ? promptedFeedbackEntryPoint : undefined;
296
+ if (entryPoint && syncBlockRemoved.length > 0 && bodiedSyncBlockRemoved.length === 0) {
297
+ tr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
298
+ context: {
299
+ blockType: 'reference',
300
+ entryPoint: entryPoint
301
+ }
302
+ });
303
+ }
269
304
  syncBlockAdded.forEach(function (syncBlock) {
270
305
  var _api$analytics2;
271
306
  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({
@@ -293,7 +328,28 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
293
328
  }
294
329
  // Only derive the mechanism behind the gate — discarded gate-off.
295
330
  var mechanism = (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
296
- return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
331
+ var feedbackContext = entryPoint && getDeleteReason(tr) !== 'source-block-unsynced' ? {
332
+ blockType: 'source',
333
+ entryPoint: entryPoint
334
+ } : undefined;
335
+ if (feedbackContext) {
336
+ var sourceAttempt = ctx.queueSourceFeedback(feedbackContext);
337
+ return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, {
338
+ onDeleteCompleted: function onDeleteCompleted(success) {
339
+ return ctx.completeSourceDeletion(sourceAttempt, success);
340
+ },
341
+ onDeleteTransaction: function onDeleteTransaction(deleteTr) {
342
+ return deleteTr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
343
+ context: feedbackContext,
344
+ sourceAttempt: sourceAttempt
345
+ });
346
+ },
347
+ onDestroy: function onDestroy() {
348
+ return ctx.clearSourceFeedback(sourceAttempt);
349
+ }
350
+ });
351
+ }
352
+ return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, undefined);
297
353
  }
298
354
  if (bodiedSyncBlockAdded.length > 0) {
299
355
  if (tr.getMeta(_utils.pmHistoryPluginKey)) {
@@ -415,15 +471,18 @@ var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore
415
471
  * is explicitly scoped to a single plugin instance.
416
472
  */
417
473
  var SyncedBlockPluginContext = /*#__PURE__*/function () {
418
- function SyncedBlockPluginContext() {
474
+ function SyncedBlockPluginContext(onGiveFeedback) {
419
475
  (0, _classCallCheck2.default)(this, SyncedBlockPluginContext);
420
476
  (0, _defineProperty2.default)(this, "confirmationTransactionRef", {
421
477
  current: undefined
422
478
  });
479
+ (0, _defineProperty2.default)(this, "_cutRemovalPending", false);
480
+ (0, _defineProperty2.default)(this, "cutRemovalGeneration", 0);
423
481
  (0, _defineProperty2.default)(this, "_isCopyEvent", false);
424
482
  (0, _defineProperty2.default)(this, "_isCutEvent", false);
425
483
  (0, _defineProperty2.default)(this, "unpublishedFlagShown", new Set());
426
484
  (0, _defineProperty2.default)(this, "extensionFlagShown", new Set());
485
+ this.onGiveFeedback = onGiveFeedback;
427
486
  }
428
487
  return (0, _createClass2.default)(SyncedBlockPluginContext, [{
429
488
  key: "isCopyEvent",
@@ -454,19 +513,114 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
454
513
  this._isCutEvent = false;
455
514
  return was;
456
515
  }
516
+ }, {
517
+ key: "markCutRemovalPending",
518
+ value: function markCutRemovalPending() {
519
+ var _this = this;
520
+ this._cutRemovalPending = true;
521
+ var generation = ++this.cutRemovalGeneration;
522
+ queueMicrotask(function () {
523
+ if (_this.cutRemovalGeneration === generation) {
524
+ _this._cutRemovalPending = false;
525
+ }
526
+ });
527
+ }
528
+ }, {
529
+ key: "consumeCutRemoval",
530
+ value: function consumeCutRemoval() {
531
+ var wasPending = this._cutRemovalPending;
532
+ this._cutRemovalPending = false;
533
+ this.cutRemovalGeneration += 1;
534
+ return wasPending;
535
+ }
536
+ }, {
537
+ key: "queueSourceFeedback",
538
+ value: function queueSourceFeedback(context) {
539
+ var sourceAttempt = Symbol('syncedBlockSourceFeedbackAttempt');
540
+ this.pendingSourceFeedback = {
541
+ context: context,
542
+ deletionSucceeded: false,
543
+ sourceAttempt: sourceAttempt,
544
+ transactionApplied: false
545
+ };
546
+ return sourceAttempt;
547
+ }
548
+ }, {
549
+ key: "completeSourceDeletion",
550
+ value: function completeSourceDeletion(sourceAttempt, success) {
551
+ var _this$pendingSourceFe;
552
+ if (((_this$pendingSourceFe = this.pendingSourceFeedback) === null || _this$pendingSourceFe === void 0 ? void 0 : _this$pendingSourceFe.sourceAttempt) !== sourceAttempt) {
553
+ return;
554
+ }
555
+ if (!success) {
556
+ // Keep the attempt pending because retryDeletion reuses these callbacks.
557
+ // Dismissal or cancellation clears it through the destroy callback.
558
+ return;
559
+ }
560
+ this.pendingSourceFeedback.deletionSucceeded = true;
561
+ this.flushSourceFeedback(sourceAttempt);
562
+ }
563
+ }, {
564
+ key: "clearSourceFeedback",
565
+ value: function clearSourceFeedback(sourceAttempt) {
566
+ var _this$pendingSourceFe2;
567
+ if (((_this$pendingSourceFe2 = this.pendingSourceFeedback) === null || _this$pendingSourceFe2 === void 0 ? void 0 : _this$pendingSourceFe2.sourceAttempt) === sourceAttempt) {
568
+ this.pendingSourceFeedback = undefined;
569
+ }
570
+ }
571
+ }, {
572
+ key: "handleAppliedFeedback",
573
+ value: function handleAppliedFeedback(_ref6) {
574
+ var _this$pendingSourceFe3;
575
+ var context = _ref6.context,
576
+ sourceAttempt = _ref6.sourceAttempt;
577
+ if (context.blockType === 'source' && sourceAttempt && ((_this$pendingSourceFe3 = this.pendingSourceFeedback) === null || _this$pendingSourceFe3 === void 0 ? void 0 : _this$pendingSourceFe3.sourceAttempt) === sourceAttempt) {
578
+ this.pendingSourceFeedback.transactionApplied = true;
579
+ this.flushSourceFeedback(sourceAttempt);
580
+ return;
581
+ }
582
+ if (context.blockType === 'reference') {
583
+ this.invokeFeedback(context);
584
+ }
585
+ }
586
+ }, {
587
+ key: "flushSourceFeedback",
588
+ value: function flushSourceFeedback(sourceAttempt) {
589
+ var _this$pendingSourceFe4, _this$pendingSourceFe5;
590
+ 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) {
591
+ var context = this.pendingSourceFeedback.context;
592
+ this.pendingSourceFeedback = undefined;
593
+ this.invokeFeedback(context);
594
+ }
595
+ }
596
+ }, {
597
+ key: "invokeFeedback",
598
+ value: function invokeFeedback(context) {
599
+ var _this2 = this;
600
+ (0, _utils3.deferDispatch)(function () {
601
+ try {
602
+ var _this2$onGiveFeedback;
603
+ var feedbackResult = (_this2$onGiveFeedback = _this2.onGiveFeedback) === null || _this2$onGiveFeedback === void 0 ? void 0 : _this2$onGiveFeedback.call(_this2, context);
604
+ void Promise.resolve(feedbackResult).catch(function () {});
605
+ } catch (_unused) {
606
+ // Product feedback collectors are optional UI. Their failures must
607
+ // never affect the already-applied editor transaction.
608
+ }
609
+ });
610
+ }
457
611
  }]);
458
612
  }();
459
613
  var createPlugin = exports.createPlugin = function createPlugin(options, pmPluginFactoryParams, syncBlockStore, api) {
460
- var _ref6 = options || {},
461
- _ref6$useLongPressSel = _ref6.useLongPressSelection,
462
- useLongPressSelection = _ref6$useLongPressSel === void 0 ? false : _ref6$useLongPressSel;
614
+ var _ref7 = options || {},
615
+ _ref7$useLongPressSel = _ref7.useLongPressSelection,
616
+ useLongPressSelection = _ref7$useLongPressSel === void 0 ? false : _ref7$useLongPressSel;
463
617
 
464
618
  // Cache the experiment value once at plugin creation time.
465
619
  // This fires the exposure event exactly once (correct per Exposure Events 101)
466
620
  // and avoids ~10 redundant Statsig SDK evaluations per keystroke in hot paths
467
621
  // (apply, filterTransaction, appendTransaction, decorations).
468
622
  var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
469
- var ctx = new SyncedBlockPluginContext();
623
+ var ctx = new SyncedBlockPluginContext(options === null || options === void 0 ? void 0 : options.onGiveFeedback);
470
624
  var confirmationTransactionRef = ctx.confirmationTransactionRef;
471
625
  var unpublishedFlagShown = ctx.unpublishedFlagShown;
472
626
  var extensionFlagShown = ctx.extensionFlagShown;
@@ -476,8 +630,8 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
476
630
  // Classic pages with sync blocks.
477
631
  syncBlockStore.sourceManager.registerFlushCompletionCallback(function () {
478
632
  (0, _utils3.deferDispatch)(function () {
479
- api === null || api === void 0 || api.core.actions.execute(function (_ref7) {
480
- var tr = _ref7.tr;
633
+ api === null || api === void 0 || api.core.actions.execute(function (_ref8) {
634
+ var tr = _ref8.tr;
481
635
  return tr;
482
636
  });
483
637
  });
@@ -505,8 +659,8 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
505
659
  // case the Confluence default is used.
506
660
  var sourceProduct = (0, _utils2.getSourceProductFromResourceIdSafe)(resourceId);
507
661
  (0, _utils3.deferDispatch)(function () {
508
- api === null || api === void 0 || api.core.actions.execute(function (_ref8) {
509
- var tr = _ref8.tr;
662
+ api === null || api === void 0 || api.core.actions.execute(function (_ref9) {
663
+ var tr = _ref9.tr;
510
664
  return tr.setMeta(syncedBlockPluginKey, {
511
665
  activeFlag: {
512
666
  id: _types.FLAG_ID.UNPUBLISHED_SYNC_BLOCK_PASTED,
@@ -818,7 +972,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
818
972
  return false;
819
973
  },
820
974
  cut: function cut() {
821
- if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_13')) {
975
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_13') || Boolean(options === null || options === void 0 ? void 0 : options.onGiveFeedback) && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true)) {
822
976
  ctx.consumeCutEvent();
823
977
  ctx.markCutEvent();
824
978
  }
@@ -849,27 +1003,47 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
849
1003
  return node;
850
1004
  });
851
1005
  },
852
- transformCopied: function transformCopied(slice, _ref9) {
853
- var state = _ref9.state;
1006
+ transformCopied: function transformCopied(slice, _ref0) {
1007
+ var state = _ref0.state;
854
1008
  var pluginState = syncedBlockPluginKey.getState(state);
855
1009
  var syncBlockStore = pluginState === null || pluginState === void 0 ? void 0 : pluginState.syncBlockStore;
856
1010
  var schema = state.schema;
857
1011
  var isCopy = ctx.consumeCopyEvent();
858
1012
  var isSyncedBlockPatch13Enabled = (0, _platformFeatureFlags.fg)('platform_synced_block_patch_13');
859
- var isCut = isSyncedBlockPatch13Enabled && ctx.consumeCutEvent();
860
- if (!syncBlockStore || !isCopy && !isCut) {
1013
+ var wasCut = ctx.consumeCutEvent();
1014
+ var isCut = isSyncedBlockPatch13Enabled && wasCut;
1015
+ var isPromptedFeedbackCut = Boolean(syncBlockStore && (options === null || options === void 0 ? void 0 : options.onGiveFeedback) && wasCut) && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true);
1016
+ if (!syncBlockStore || !isCopy && !isCut && !isPromptedFeedbackCut) {
861
1017
  return slice;
862
1018
  }
863
- return (0, _utils.mapSlice)(slice, function (node) {
1019
+ if (isPromptedFeedbackCut && !isCut) {
1020
+ var _containsCutSyncBlock = false;
1021
+ (0, _utils.mapSlice)(slice, function (node) {
1022
+ if (syncBlockStore.referenceManager.isReferenceBlock(node) || node.type.name === 'bodiedSyncBlock' && (0, _utils3.sliceFullyContainsNode)(slice, node)) {
1023
+ _containsCutSyncBlock = true;
1024
+ }
1025
+ return node;
1026
+ });
1027
+ if (_containsCutSyncBlock) {
1028
+ ctx.markCutRemovalPending();
1029
+ }
1030
+ return slice;
1031
+ }
1032
+ var containsCutSyncBlock = false;
1033
+ var transformedSlice = (0, _utils.mapSlice)(slice, function (node) {
864
1034
  if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
865
1035
  var _syncBlockStore$refer;
866
1036
  if (isCut) {
1037
+ containsCutSyncBlock = isPromptedFeedbackCut;
867
1038
  return node;
868
1039
  }
869
1040
  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', (0, _utils2.getSourceProductFromResourceIdSafe)(node.attrs.resourceId));
870
1041
  return node;
871
1042
  }
872
1043
  if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
1044
+ if (isPromptedFeedbackCut && isCut && (0, _utils3.sliceFullyContainsNode)(slice, node)) {
1045
+ containsCutSyncBlock = true;
1046
+ }
873
1047
  // if we only selected part of the bodied sync block content,
874
1048
  // remove the sync block node and only keep the content
875
1049
  if (!(0, _utils3.sliceFullyContainsNode)(slice, node)) {
@@ -893,6 +1067,10 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
893
1067
  }
894
1068
  return node;
895
1069
  });
1070
+ if (containsCutSyncBlock) {
1071
+ ctx.markCutRemovalPending();
1072
+ }
1073
+ return transformedSlice;
896
1074
  }
897
1075
  },
898
1076
  filterTransaction: function filterTransaction(tr, state) {
@@ -979,6 +1157,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
979
1157
  syncBlockStore: syncBlockStore,
980
1158
  api: api,
981
1159
  confirmationTransactionRef: confirmationTransactionRef,
1160
+ ctx: ctx,
982
1161
  bodiedSyncBlockRemoved: bodiedSyncBlockRemoved,
983
1162
  bodiedSyncBlockAdded: bodiedSyncBlockAdded,
984
1163
  extensionFlagShown: extensionFlagShown
@@ -1002,6 +1181,17 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
1002
1181
  if (viewMode === 'view') {
1003
1182
  return null;
1004
1183
  }
1184
+ if (options !== null && options !== void 0 && options.onGiveFeedback) {
1185
+ var feedbackContexts = trs.flatMap(function (tr) {
1186
+ var feedbackContext = tr.getMeta(syncedBlockPromptedFeedbackMetaKey);
1187
+ return feedbackContext ? [feedbackContext] : [];
1188
+ });
1189
+ if (feedbackContexts.length > 0 && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true)) {
1190
+ feedbackContexts.forEach(function (feedbackContext) {
1191
+ ctx.handleAppliedFeedback(feedbackContext);
1192
+ });
1193
+ }
1194
+ }
1005
1195
 
1006
1196
  // Update source sync block cache for user-initiated changes only.
1007
1197
  // Cache updates are handled here instead of in the nodeview update() so we
@@ -1086,8 +1276,8 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
1086
1276
  if (!(step instanceof _transform.ReplaceStep || step instanceof _transform.ReplaceAroundStep) || !('slice' in step)) {
1087
1277
  return false;
1088
1278
  }
1089
- var _ref0 = step,
1090
- slice = _ref0.slice;
1279
+ var _ref1 = step,
1280
+ slice = _ref1.slice;
1091
1281
  var found = false;
1092
1282
  slice.content.descendants(function (node) {
1093
1283
  if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
@@ -1127,8 +1317,8 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
1127
1317
  tr.setMeta('addToHistory', false);
1128
1318
  (0, _utils3.deferDispatch)(function () {
1129
1319
  var _api$core;
1130
- api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref1) {
1131
- var tr = _ref1.tr;
1320
+ api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref10) {
1321
+ var tr = _ref10.tr;
1132
1322
  return tr.setMeta(syncedBlockPluginKey, {
1133
1323
  activeFlag: {
1134
1324
  id: _types.FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK
@@ -34,7 +34,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
34
34
  });
35
35
  };
36
36
  };
37
- var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
37
+ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism, feedbackCallbacks) {
38
38
  // Clear potential old pending deletion to retreat the deletion as first attempt
39
39
  syncBlockStore.sourceManager.clearPendingDeletion();
40
40
 
@@ -56,7 +56,15 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
56
56
  // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
57
57
  api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
58
58
  var tr = _ref2.tr;
59
- var recomputedTr = (0, _recomputeDeleteTransaction.recomputeDeleteTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
59
+ // EDITOR-8230: unsync must remove only the sync wrapper and keep the block's
60
+ // content inline, whereas delete removes the whole block. Before this branch,
61
+ // the confirm path always recomputed a full delete, so unsyncing a source block
62
+ // silently deleted its content. When the deletion was actually triggered by
63
+ // unsync, recompute an unwrap (replaceWith content) instead of a delete.
64
+ var isUnsync = (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_6') && deletionReason === 'source-block-unsynced';
65
+ var recomputedTr = isUnsync ? (0, _recomputeDeleteTransaction.recomputeUnsyncTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
66
+ return node.attrs;
67
+ })) : (0, _recomputeDeleteTransaction.recomputeDeleteTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
60
68
  return node.attrs;
61
69
  }));
62
70
 
@@ -70,6 +78,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
70
78
  return null;
71
79
  }
72
80
  recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
81
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(recomputedTr);
73
82
  if (!recomputedTr.getMeta(_utils.pmHistoryPluginKey)) {
74
83
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
75
84
  // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
@@ -85,6 +94,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
85
94
  }
86
95
  api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function () {
87
96
  var trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
97
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(trToDispatch);
88
98
  if (!trToDispatch.getMeta(_utils.pmHistoryPluginKey)) {
89
99
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
90
100
  // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
@@ -94,6 +104,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
94
104
  });
95
105
  }, function (success) {
96
106
  var _api$core4;
107
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteCompleted(success);
97
108
  api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
98
109
  var tr = _ref3.tr;
99
110
  var newState;
@@ -121,6 +132,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
121
132
  });
122
133
  }, function () {
123
134
  confirmationTransactionRef.current = undefined;
135
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDestroy();
124
136
  }, mechanism);
125
137
  return false;
126
138
  };
@@ -3,7 +3,11 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.recomputeDeleteTransaction = void 0;
6
+ exports.recomputeUnsyncTransaction = exports.recomputeDeleteTransaction = void 0;
7
+ var _model = require("@atlaskit/editor-prosemirror/model");
8
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
9
+ 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; } }
10
+ 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; }
7
11
  /**
8
12
  * Whether `node` is the source block requested by `wanted`.
9
13
  *
@@ -95,4 +99,65 @@ var recomputeDeleteTransaction = exports.recomputeDeleteTransaction = function r
95
99
  tr.delete(pos, pos + node.nodeSize);
96
100
  });
97
101
  return tr;
102
+ };
103
+
104
+ /**
105
+ * Recompute a source `bodiedSyncBlock` *unsync* from the live document.
106
+ *
107
+ * Unsync differs from delete: the sync wrapper must be removed while its content
108
+ * is preserved inline in the document. Instead of `tr.delete(pos, pos +
109
+ * nodeSize)` (which drops the content too — EDITOR-8230), this replaces each
110
+ * matched source block with its own content.
111
+ *
112
+ * The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content,
113
+ * 0, 0))` — an explicit zero-open slice inserted verbatim — rather than
114
+ * `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's
115
+ * `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing
116
+ * block (e.g. the block's final panel/empty paragraph) when the unwrapped
117
+ * content meets a document boundary, silently losing content (EDITOR-8230). A
118
+ * zero-open `replace` drops the wrapper's own open ends while keeping every child
119
+ * of the fragment as its own top-level node.
120
+ *
121
+ * Like `recomputeDeleteTransaction`, targets are located in the *current*
122
+ * document (`tr.doc`) so the operation is robust to intervening local edits and
123
+ * remote collab changes while the confirmation modal is open, and it handles
124
+ * partial matches gracefully (missing blocks are skipped).
125
+ *
126
+ * Replacements are applied in reverse document order so earlier replacements do
127
+ * not invalidate the positions of later ones.
128
+ *
129
+ * @returns the mutated transaction when at least one target node was found and
130
+ * unwrapped, otherwise `undefined` when none of the targets exist any more.
131
+ */
132
+ var recomputeUnsyncTransaction = exports.recomputeUnsyncTransaction = function recomputeUnsyncTransaction(tr, isSourceBlock, syncBlockIds) {
133
+ var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
134
+ if (matches.length === 0) {
135
+ return undefined;
136
+ }
137
+
138
+ // Unwrap in reverse document order so positions remain valid across edits.
139
+ // Use a raw zero-open Slice replace (not `replaceWith`) so the block's content
140
+ // is inserted verbatim as top-level nodes without ProseMirror's replaceRange
141
+ // heuristics collapsing a trailing block (EDITOR-8230).
142
+ var orderedMatches = matches.sort(function (a, b) {
143
+ return b.pos - a.pos;
144
+ });
145
+ var _iterator = _createForOfIteratorHelper(orderedMatches),
146
+ _step;
147
+ try {
148
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
149
+ var _step$value = _step.value,
150
+ node = _step$value.node,
151
+ pos = _step$value.pos;
152
+ // This is ProseMirror's Transaction.replace (a document edit), not String.replace — the
153
+ // perf rule misfires on the method name, and there is nothing to hoist.
154
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
155
+ tr.replace(pos, pos + node.nodeSize, new _model.Slice(node.content, 0, 0));
156
+ }
157
+ } catch (err) {
158
+ _iterator.e(err);
159
+ } finally {
160
+ _iterator.f();
161
+ }
162
+ return tr;
98
163
  };