@atlaskit/editor-plugin-synced-block 8.4.2 → 8.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 8.4.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`0805dc02ccee3`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/0805dc02ccee3) -
8
+ Fix partial cut handling for source synced blocks behind platform_synced_block_patch_13.
9
+ - Updated dependencies
10
+
11
+ ## 8.4.3
12
+
13
+ ### Patch Changes
14
+
15
+ - [`085a281306c03`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/085a281306c03) -
16
+ Add defensive mechanisms for synced block EntityNotFound errors:
17
+ - Add retry with exponential backoff when fetching synced block references returns EntityNotFound
18
+ (up to 3 retries with 2s/4s/8s delays)
19
+ - Add transformPasted handler to convert any bodiedSyncBlock nodes arriving via paste into
20
+ syncBlock references, preventing createBlock from being called with the wrong parentId
21
+
22
+ Both changes are gated behind `platform_synced_block_patch_13`.
23
+
24
+ - Updated dependencies
25
+
3
26
  ## 8.4.2
4
27
 
5
28
  ### Patch Changes
@@ -18,9 +18,10 @@ var _utils = require("@atlaskit/editor-common/utils");
18
18
  var _editorPluginConnectivity = require("@atlaskit/editor-plugin-connectivity");
19
19
  var _state = require("@atlaskit/editor-prosemirror/state");
20
20
  var _transform = require("@atlaskit/editor-prosemirror/transform");
21
- var _view = require("@atlaskit/editor-prosemirror/view");
21
+ var _view2 = require("@atlaskit/editor-prosemirror/view");
22
22
  var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
23
23
  var _utils2 = require("@atlaskit/editor-synced-block-provider/utils");
24
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
24
25
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
25
26
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
26
27
  var _bodiedSyncedBlock = require("../nodeviews/bodiedSyncedBlock");
@@ -153,6 +154,7 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
153
154
  // events this lets us triangulate cross-product paste behaviour without standing up a
154
155
  // dedicated `cross_product_paste` event subject.
155
156
  var isPaste = Boolean((_tr$getMeta = tr.getMeta('paste')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : tr.getMeta('uiEvent') === 'paste');
157
+ var isPasteOrDrop = isPaste || tr.getMeta('uiEvent') === 'drop';
156
158
  syncBlockAdded.forEach(function (syncBlock) {
157
159
  var _api$analytics2;
158
160
  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({
@@ -180,6 +182,16 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
180
182
  // After true is returned here and the node is created, we delete the node in the filterTransaction immediately, which cancels out the creation
181
183
  return true;
182
184
  }
185
+
186
+ // Defense-in-depth: if a bodiedSyncBlock arrives via paste or drag-and-drop,
187
+ // it may have bypassed transformCopied (e.g. cut, browser clipboard).
188
+ // Do not call createBlock — it would use this page's parentId which may
189
+ // be wrong. The bodiedSyncBlock content will still be inserted but will
190
+ // not be registered as a source block in Block Service, which is safer
191
+ // than creating a zombie block under the wrong ARI.
192
+ if (isPasteOrDrop && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_13')) {
193
+ return true;
194
+ }
183
195
  (0, _handleBodiedSyncBlockCreation.handleBodiedSyncBlockCreation)(bodiedSyncBlockAdded, state, api);
184
196
  return true;
185
197
  }
@@ -232,7 +244,7 @@ var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore
232
244
  // Fast path: when all status flags are off and no creations are in flight,
233
245
  // no node can produce a decoration — skip the full doc traversal.
234
246
  if (!isOffline && !isViewMode && !isDragging && !syncBlockStore.sourceManager.hasPendingCreations()) {
235
- return _view.DecorationSet.empty;
247
+ return _view2.DecorationSet.empty;
236
248
  }
237
249
  var offlineDecorations = [];
238
250
  var viewModeDecorations = [];
@@ -240,22 +252,22 @@ var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore
240
252
  var dragDecorations = [];
241
253
  doc.descendants(function (node, pos) {
242
254
  if (node.type.name === 'bodiedSyncBlock' && isOffline) {
243
- offlineDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
255
+ offlineDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
244
256
  class: _syncBlock.SyncBlockStateCssClassName.disabledClassName
245
257
  }));
246
258
  }
247
259
  if (syncBlockStore.isSyncBlock(node) && isViewMode) {
248
- viewModeDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
260
+ viewModeDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
249
261
  class: _syncBlock.SyncBlockStateCssClassName.viewModeClassName
250
262
  }));
251
263
  }
252
264
  if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
253
- loadingDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
265
+ loadingDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
254
266
  class: _syncBlock.SyncBlockStateCssClassName.creationLoadingClassName
255
267
  }));
256
268
  }
257
269
  if (isDragging && syncBlockStore.isSyncBlock(node)) {
258
- dragDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
270
+ dragDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
259
271
  class: _syncBlock.SyncBlockStateCssClassName.draggingClassName
260
272
  }));
261
273
  }
@@ -263,7 +275,7 @@ var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore
263
275
  // only traverse the top-level node of the document, as syncBlock and bodiedSyncBlock are top-level nodes
264
276
  return false;
265
277
  });
266
- return _view.DecorationSet.create(doc, [].concat(offlineDecorations, viewModeDecorations, loadingDecorations, dragDecorations));
278
+ return _view2.DecorationSet.create(doc, [].concat(offlineDecorations, viewModeDecorations, loadingDecorations, dragDecorations));
267
279
  };
268
280
 
269
281
  /**
@@ -278,6 +290,7 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
278
290
  current: undefined
279
291
  });
280
292
  (0, _defineProperty2.default)(this, "_isCopyEvent", false);
293
+ (0, _defineProperty2.default)(this, "_isCutEvent", false);
281
294
  (0, _defineProperty2.default)(this, "unpublishedFlagShown", new Set());
282
295
  (0, _defineProperty2.default)(this, "extensionFlagShown", new Set());
283
296
  }
@@ -291,6 +304,11 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
291
304
  value: function markCopyEvent() {
292
305
  this._isCopyEvent = true;
293
306
  }
307
+ }, {
308
+ key: "markCutEvent",
309
+ value: function markCutEvent() {
310
+ this._isCutEvent = true;
311
+ }
294
312
  }, {
295
313
  key: "consumeCopyEvent",
296
314
  value: function consumeCopyEvent() {
@@ -298,6 +316,13 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
298
316
  this._isCopyEvent = false;
299
317
  return was;
300
318
  }
319
+ }, {
320
+ key: "consumeCutEvent",
321
+ value: function consumeCutEvent() {
322
+ var was = this._isCutEvent;
323
+ this._isCutEvent = false;
324
+ return was;
325
+ }
301
326
  }]);
302
327
  }();
303
328
  var createPlugin = exports.createPlugin = function createPlugin(options, pmPluginFactoryParams, syncBlockStore, api) {
@@ -396,7 +421,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
396
421
  // When the perf gate is ON and the doc has synced blocks we do a
397
422
  // single traversal here; afterwards `apply()` will map or rebuild
398
423
  // only when a status signal changes.
399
- var initStatusDecorationSet = docHasSyncedBlocks && isPerfExperimentOn ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view.DecorationSet.empty;
424
+ var initStatusDecorationSet = docHasSyncedBlocks && isPerfExperimentOn ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view2.DecorationSet.empty;
400
425
  return {
401
426
  selectionDecorationSet: (0, _selectionDecorations.calculateDecorations)(instance.doc, instance.selection, instance.schema),
402
427
  activeFlag: false,
@@ -482,7 +507,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
482
507
  if (isPerfExperimentOn) {
483
508
  if (!nextHasSyncedBlocks) {
484
509
  // No synced blocks → keep empty status decorations
485
- nextStatusDecorationSet = _view.DecorationSet.empty;
510
+ nextStatusDecorationSet = _view2.DecorationSet.empty;
486
511
  } else {
487
512
  var _api$connectivity4, _api$editorViewMode3, _api$userIntent3;
488
513
  // Read current shared-state signals
@@ -570,7 +595,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
570
595
  decorations: function decorations(state) {
571
596
  var currentPluginState = syncedBlockPluginKey.getState(state);
572
597
  if (!currentPluginState) {
573
- return _view.DecorationSet.empty;
598
+ return _view2.DecorationSet.empty;
574
599
  }
575
600
  var selectionDecorationSet = currentPluginState.selectionDecorationSet,
576
601
  statusDecorationSet = currentPluginState.statusDecorationSet,
@@ -598,7 +623,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
598
623
  // omit selection decorations (matches old behaviour).
599
624
  var statusDecorations = statusDecorationSet.find();
600
625
  if (statusDecorations.length === 0) {
601
- return hasFocus ? selectionDecorationSet : _view.DecorationSet.empty;
626
+ return hasFocus ? selectionDecorationSet : _view2.DecorationSet.empty;
602
627
  } else {
603
628
  return hasFocus ? selectionDecorationSet.add(state.doc, statusDecorations) : statusDecorationSet;
604
629
  }
@@ -618,22 +643,22 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
618
643
  var dragDecorations = [];
619
644
  state.doc.descendants(function (node, pos) {
620
645
  if (node.type.name === 'bodiedSyncBlock' && isOffline) {
621
- offlineDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
646
+ offlineDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
622
647
  class: _syncBlock.SyncBlockStateCssClassName.disabledClassName
623
648
  }));
624
649
  }
625
650
  if (_syncBlockStore.isSyncBlock(node) && isViewMode) {
626
- viewModeDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
651
+ viewModeDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
627
652
  class: _syncBlock.SyncBlockStateCssClassName.viewModeClassName
628
653
  }));
629
654
  }
630
655
  if (node.type.name === 'bodiedSyncBlock' && _syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
631
- loadingDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
656
+ loadingDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
632
657
  class: _syncBlock.SyncBlockStateCssClassName.creationLoadingClassName
633
658
  }));
634
659
  }
635
660
  if (isDragging && _syncBlockStore.isSyncBlock(node)) {
636
- dragDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
661
+ dragDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
637
662
  class: _syncBlock.SyncBlockStateCssClassName.draggingClassName
638
663
  }));
639
664
  }
@@ -641,7 +666,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
641
666
  if (api !== null && api !== void 0 && (_api$focus2 = api.focus) !== null && _api$focus2 !== void 0 && (_api$focus2 = _api$focus2.sharedState) !== null && _api$focus2 !== void 0 && (_api$focus2 = _api$focus2.currentState()) !== null && _api$focus2 !== void 0 && _api$focus2.hasFocus) {
642
667
  return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
643
668
  } else {
644
- return _view.DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
669
+ return _view2.DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
645
670
  }
646
671
  }
647
672
  },
@@ -660,19 +685,55 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
660
685
  copy: function copy() {
661
686
  ctx.markCopyEvent();
662
687
  return false;
688
+ },
689
+ cut: function cut() {
690
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_13')) {
691
+ ctx.consumeCutEvent();
692
+ ctx.markCutEvent();
693
+ }
694
+ return false;
663
695
  }
664
696
  },
697
+ transformPasted: function transformPasted(slice, _view) {
698
+ if (!(0, _platformFeatureFlags.fg)('platform_synced_block_patch_13')) {
699
+ return slice;
700
+ }
701
+
702
+ // Defense against bodiedSyncBlock nodes arriving via paste
703
+ // (e.g. drag-and-drop, cut-paste, or browser-level clipboard
704
+ // operations that bypass the transformCopied handler).
705
+ // We cannot convert to a syncBlock reference here because we don't
706
+ // know the source page's parentId (generateResourceIdForReference
707
+ // would use the current page's parentId which is wrong).
708
+ // Instead, strip the bodiedSyncBlock wrapper and keep only the
709
+ // inner content to prevent createBlock being called with the
710
+ // wrong parentId.
711
+ return (0, _utils.mapSlice)(slice, function (node) {
712
+ if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
713
+ // Return the inner content without the sync block wrapper.
714
+ // This is the same behavior as when a partial selection of
715
+ // a bodiedSyncBlock is copied (see transformCopied line 937).
716
+ return node.content;
717
+ }
718
+ return node;
719
+ });
720
+ },
665
721
  transformCopied: function transformCopied(slice, _ref9) {
666
722
  var state = _ref9.state;
667
723
  var pluginState = syncedBlockPluginKey.getState(state);
668
724
  var syncBlockStore = pluginState === null || pluginState === void 0 ? void 0 : pluginState.syncBlockStore;
669
725
  var schema = state.schema;
670
726
  var isCopy = ctx.consumeCopyEvent();
671
- if (!syncBlockStore || !isCopy) {
727
+ var isSyncedBlockPatch13Enabled = (0, _platformFeatureFlags.fg)('platform_synced_block_patch_13');
728
+ var isCut = isSyncedBlockPatch13Enabled && ctx.consumeCutEvent();
729
+ if (!syncBlockStore || !isCopy && !isCut) {
672
730
  return slice;
673
731
  }
674
732
  return (0, _utils.mapSlice)(slice, function (node) {
675
733
  if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
734
+ if (isCut) {
735
+ return node;
736
+ }
676
737
  showCopiedFlag(api);
677
738
  return node;
678
739
  }
@@ -682,6 +743,9 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
682
743
  if (!(0, _utils3.sliceFullyContainsNode)(slice, node)) {
683
744
  return node.content;
684
745
  }
746
+ if (isSyncedBlockPatch13Enabled && isCut) {
747
+ return node;
748
+ }
685
749
  showCopiedFlag(api);
686
750
  var newResourceId = syncBlockStore.referenceManager.generateResourceIdForReference(node.attrs.resourceId);
687
751
  // Convert bodiedSyncBlock to syncBlock
@@ -11,6 +11,7 @@ import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/tra
11
11
  import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
12
12
  import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
13
13
  import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
14
+ import { fg } from '@atlaskit/platform-feature-flags';
14
15
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
15
16
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
16
17
  import { bodiedSyncBlockNodeView, bodiedSyncBlockNodeViewOld } from '../nodeviews/bodiedSyncedBlock';
@@ -129,6 +130,7 @@ const filterTransactionOnline = ({
129
130
  // events this lets us triangulate cross-product paste behaviour without standing up a
130
131
  // dedicated `cross_product_paste` event subject.
131
132
  const isPaste = Boolean((_tr$getMeta = tr.getMeta('paste')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : tr.getMeta('uiEvent') === 'paste');
133
+ const isPasteOrDrop = isPaste || tr.getMeta('uiEvent') === 'drop';
132
134
  syncBlockAdded.forEach(syncBlock => {
133
135
  var _api$analytics2, _api$analytics2$actio;
134
136
  api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : (_api$analytics2$actio = _api$analytics2.actions) === null || _api$analytics2$actio === void 0 ? void 0 : _api$analytics2$actio.fireAnalyticsEvent({
@@ -156,6 +158,16 @@ const filterTransactionOnline = ({
156
158
  // After true is returned here and the node is created, we delete the node in the filterTransaction immediately, which cancels out the creation
157
159
  return true;
158
160
  }
161
+
162
+ // Defense-in-depth: if a bodiedSyncBlock arrives via paste or drag-and-drop,
163
+ // it may have bypassed transformCopied (e.g. cut, browser clipboard).
164
+ // Do not call createBlock — it would use this page's parentId which may
165
+ // be wrong. The bodiedSyncBlock content will still be inserted but will
166
+ // not be registered as a source block in Block Service, which is safer
167
+ // than creating a zombie block under the wrong ARI.
168
+ if (isPasteOrDrop && fg('platform_synced_block_patch_13')) {
169
+ return true;
170
+ }
159
171
  handleBodiedSyncBlockCreation(bodiedSyncBlockAdded, state, api);
160
172
  return true;
161
173
  }
@@ -254,6 +266,7 @@ class SyncedBlockPluginContext {
254
266
  current: undefined
255
267
  });
256
268
  _defineProperty(this, "_isCopyEvent", false);
269
+ _defineProperty(this, "_isCutEvent", false);
257
270
  _defineProperty(this, "unpublishedFlagShown", new Set());
258
271
  _defineProperty(this, "extensionFlagShown", new Set());
259
272
  }
@@ -263,11 +276,19 @@ class SyncedBlockPluginContext {
263
276
  markCopyEvent() {
264
277
  this._isCopyEvent = true;
265
278
  }
279
+ markCutEvent() {
280
+ this._isCutEvent = true;
281
+ }
266
282
  consumeCopyEvent() {
267
283
  const was = this._isCopyEvent;
268
284
  this._isCopyEvent = false;
269
285
  return was;
270
286
  }
287
+ consumeCutEvent() {
288
+ const was = this._isCutEvent;
289
+ this._isCutEvent = false;
290
+ return was;
291
+ }
271
292
  }
272
293
  export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api) => {
273
294
  const {
@@ -628,7 +649,38 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
628
649
  copy: () => {
629
650
  ctx.markCopyEvent();
630
651
  return false;
652
+ },
653
+ cut: () => {
654
+ if (fg('platform_synced_block_patch_13')) {
655
+ ctx.consumeCutEvent();
656
+ ctx.markCutEvent();
657
+ }
658
+ return false;
659
+ }
660
+ },
661
+ transformPasted: (slice, _view) => {
662
+ if (!fg('platform_synced_block_patch_13')) {
663
+ return slice;
631
664
  }
665
+
666
+ // Defense against bodiedSyncBlock nodes arriving via paste
667
+ // (e.g. drag-and-drop, cut-paste, or browser-level clipboard
668
+ // operations that bypass the transformCopied handler).
669
+ // We cannot convert to a syncBlock reference here because we don't
670
+ // know the source page's parentId (generateResourceIdForReference
671
+ // would use the current page's parentId which is wrong).
672
+ // Instead, strip the bodiedSyncBlock wrapper and keep only the
673
+ // inner content to prevent createBlock being called with the
674
+ // wrong parentId.
675
+ return mapSlice(slice, node => {
676
+ if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
677
+ // Return the inner content without the sync block wrapper.
678
+ // This is the same behavior as when a partial selection of
679
+ // a bodiedSyncBlock is copied (see transformCopied line 937).
680
+ return node.content;
681
+ }
682
+ return node;
683
+ });
632
684
  },
633
685
  transformCopied: (slice, {
634
686
  state
@@ -639,11 +691,16 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
639
691
  schema
640
692
  } = state;
641
693
  const isCopy = ctx.consumeCopyEvent();
642
- if (!syncBlockStore || !isCopy) {
694
+ const isSyncedBlockPatch13Enabled = fg('platform_synced_block_patch_13');
695
+ const isCut = isSyncedBlockPatch13Enabled && ctx.consumeCutEvent();
696
+ if (!syncBlockStore || !isCopy && !isCut) {
643
697
  return slice;
644
698
  }
645
699
  return mapSlice(slice, node => {
646
700
  if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
701
+ if (isCut) {
702
+ return node;
703
+ }
647
704
  showCopiedFlag(api);
648
705
  return node;
649
706
  }
@@ -653,6 +710,9 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
653
710
  if (!sliceFullyContainsNode(slice, node)) {
654
711
  return node.content;
655
712
  }
713
+ if (isSyncedBlockPatch13Enabled && isCut) {
714
+ return node;
715
+ }
656
716
  showCopiedFlag(api);
657
717
  const newResourceId = syncBlockStore.referenceManager.generateResourceIdForReference(node.attrs.resourceId);
658
718
  // Convert bodiedSyncBlock to syncBlock
@@ -19,6 +19,7 @@ import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/tra
19
19
  import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
20
20
  import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
21
21
  import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
22
+ import { fg } from '@atlaskit/platform-feature-flags';
22
23
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
23
24
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
24
25
  import { bodiedSyncBlockNodeView, bodiedSyncBlockNodeViewOld } from '../nodeviews/bodiedSyncedBlock';
@@ -146,6 +147,7 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
146
147
  // events this lets us triangulate cross-product paste behaviour without standing up a
147
148
  // dedicated `cross_product_paste` event subject.
148
149
  var isPaste = Boolean((_tr$getMeta = tr.getMeta('paste')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : tr.getMeta('uiEvent') === 'paste');
150
+ var isPasteOrDrop = isPaste || tr.getMeta('uiEvent') === 'drop';
149
151
  syncBlockAdded.forEach(function (syncBlock) {
150
152
  var _api$analytics2;
151
153
  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({
@@ -173,6 +175,16 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
173
175
  // After true is returned here and the node is created, we delete the node in the filterTransaction immediately, which cancels out the creation
174
176
  return true;
175
177
  }
178
+
179
+ // Defense-in-depth: if a bodiedSyncBlock arrives via paste or drag-and-drop,
180
+ // it may have bypassed transformCopied (e.g. cut, browser clipboard).
181
+ // Do not call createBlock — it would use this page's parentId which may
182
+ // be wrong. The bodiedSyncBlock content will still be inserted but will
183
+ // not be registered as a source block in Block Service, which is safer
184
+ // than creating a zombie block under the wrong ARI.
185
+ if (isPasteOrDrop && fg('platform_synced_block_patch_13')) {
186
+ return true;
187
+ }
176
188
  handleBodiedSyncBlockCreation(bodiedSyncBlockAdded, state, api);
177
189
  return true;
178
190
  }
@@ -271,6 +283,7 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
271
283
  current: undefined
272
284
  });
273
285
  _defineProperty(this, "_isCopyEvent", false);
286
+ _defineProperty(this, "_isCutEvent", false);
274
287
  _defineProperty(this, "unpublishedFlagShown", new Set());
275
288
  _defineProperty(this, "extensionFlagShown", new Set());
276
289
  }
@@ -284,6 +297,11 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
284
297
  value: function markCopyEvent() {
285
298
  this._isCopyEvent = true;
286
299
  }
300
+ }, {
301
+ key: "markCutEvent",
302
+ value: function markCutEvent() {
303
+ this._isCutEvent = true;
304
+ }
287
305
  }, {
288
306
  key: "consumeCopyEvent",
289
307
  value: function consumeCopyEvent() {
@@ -291,6 +309,13 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
291
309
  this._isCopyEvent = false;
292
310
  return was;
293
311
  }
312
+ }, {
313
+ key: "consumeCutEvent",
314
+ value: function consumeCutEvent() {
315
+ var was = this._isCutEvent;
316
+ this._isCutEvent = false;
317
+ return was;
318
+ }
294
319
  }]);
295
320
  }();
296
321
  export var createPlugin = function createPlugin(options, pmPluginFactoryParams, syncBlockStore, api) {
@@ -653,19 +678,55 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
653
678
  copy: function copy() {
654
679
  ctx.markCopyEvent();
655
680
  return false;
681
+ },
682
+ cut: function cut() {
683
+ if (fg('platform_synced_block_patch_13')) {
684
+ ctx.consumeCutEvent();
685
+ ctx.markCutEvent();
686
+ }
687
+ return false;
656
688
  }
657
689
  },
690
+ transformPasted: function transformPasted(slice, _view) {
691
+ if (!fg('platform_synced_block_patch_13')) {
692
+ return slice;
693
+ }
694
+
695
+ // Defense against bodiedSyncBlock nodes arriving via paste
696
+ // (e.g. drag-and-drop, cut-paste, or browser-level clipboard
697
+ // operations that bypass the transformCopied handler).
698
+ // We cannot convert to a syncBlock reference here because we don't
699
+ // know the source page's parentId (generateResourceIdForReference
700
+ // would use the current page's parentId which is wrong).
701
+ // Instead, strip the bodiedSyncBlock wrapper and keep only the
702
+ // inner content to prevent createBlock being called with the
703
+ // wrong parentId.
704
+ return mapSlice(slice, function (node) {
705
+ if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
706
+ // Return the inner content without the sync block wrapper.
707
+ // This is the same behavior as when a partial selection of
708
+ // a bodiedSyncBlock is copied (see transformCopied line 937).
709
+ return node.content;
710
+ }
711
+ return node;
712
+ });
713
+ },
658
714
  transformCopied: function transformCopied(slice, _ref9) {
659
715
  var state = _ref9.state;
660
716
  var pluginState = syncedBlockPluginKey.getState(state);
661
717
  var syncBlockStore = pluginState === null || pluginState === void 0 ? void 0 : pluginState.syncBlockStore;
662
718
  var schema = state.schema;
663
719
  var isCopy = ctx.consumeCopyEvent();
664
- if (!syncBlockStore || !isCopy) {
720
+ var isSyncedBlockPatch13Enabled = fg('platform_synced_block_patch_13');
721
+ var isCut = isSyncedBlockPatch13Enabled && ctx.consumeCutEvent();
722
+ if (!syncBlockStore || !isCopy && !isCut) {
665
723
  return slice;
666
724
  }
667
725
  return mapSlice(slice, function (node) {
668
726
  if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
727
+ if (isCut) {
728
+ return node;
729
+ }
669
730
  showCopiedFlag(api);
670
731
  return node;
671
732
  }
@@ -675,6 +736,9 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
675
736
  if (!sliceFullyContainsNode(slice, node)) {
676
737
  return node.content;
677
738
  }
739
+ if (isSyncedBlockPatch13Enabled && isCut) {
740
+ return node;
741
+ }
678
742
  showCopiedFlag(api);
679
743
  var newResourceId = syncBlockStore.referenceManager.generateResourceIdForReference(node.attrs.resourceId);
680
744
  // Convert bodiedSyncBlock to syncBlock
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "8.4.2",
3
+ "version": "8.4.4",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/adf-schema": "^52.14.0",
32
32
  "@atlaskit/button": "23.11.8",
33
- "@atlaskit/dropdown-menu": "16.9.4",
33
+ "@atlaskit/dropdown-menu": "16.9.5",
34
34
  "@atlaskit/editor-json-transformer": "^8.32.0",
35
35
  "@atlaskit/editor-plugin-analytics": "^10.1.0",
36
36
  "@atlaskit/editor-plugin-block-menu": "^9.2.0",
@@ -64,7 +64,7 @@
64
64
  "date-fns": "^2.17.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@atlaskit/editor-common": "^114.46.0",
67
+ "@atlaskit/editor-common": "^114.47.0",
68
68
  "react": "^18.2.0",
69
69
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
70
70
  },