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

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,20 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 8.4.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`085a281306c03`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/085a281306c03) -
8
+ Add defensive mechanisms for synced block EntityNotFound errors:
9
+ - Add retry with exponential backoff when fetching synced block references returns EntityNotFound
10
+ (up to 3 retries with 2s/4s/8s delays)
11
+ - Add transformPasted handler to convert any bodiedSyncBlock nodes arriving via paste into
12
+ syncBlock references, preventing createBlock from being called with the wrong parentId
13
+
14
+ Both changes are gated behind `platform_synced_block_patch_13`.
15
+
16
+ - Updated dependencies
17
+
3
18
  ## 8.4.2
4
19
 
5
20
  ### 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
  /**
@@ -396,7 +408,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
396
408
  // When the perf gate is ON and the doc has synced blocks we do a
397
409
  // single traversal here; afterwards `apply()` will map or rebuild
398
410
  // only when a status signal changes.
399
- var initStatusDecorationSet = docHasSyncedBlocks && isPerfExperimentOn ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view.DecorationSet.empty;
411
+ var initStatusDecorationSet = docHasSyncedBlocks && isPerfExperimentOn ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view2.DecorationSet.empty;
400
412
  return {
401
413
  selectionDecorationSet: (0, _selectionDecorations.calculateDecorations)(instance.doc, instance.selection, instance.schema),
402
414
  activeFlag: false,
@@ -482,7 +494,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
482
494
  if (isPerfExperimentOn) {
483
495
  if (!nextHasSyncedBlocks) {
484
496
  // No synced blocks → keep empty status decorations
485
- nextStatusDecorationSet = _view.DecorationSet.empty;
497
+ nextStatusDecorationSet = _view2.DecorationSet.empty;
486
498
  } else {
487
499
  var _api$connectivity4, _api$editorViewMode3, _api$userIntent3;
488
500
  // Read current shared-state signals
@@ -570,7 +582,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
570
582
  decorations: function decorations(state) {
571
583
  var currentPluginState = syncedBlockPluginKey.getState(state);
572
584
  if (!currentPluginState) {
573
- return _view.DecorationSet.empty;
585
+ return _view2.DecorationSet.empty;
574
586
  }
575
587
  var selectionDecorationSet = currentPluginState.selectionDecorationSet,
576
588
  statusDecorationSet = currentPluginState.statusDecorationSet,
@@ -598,7 +610,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
598
610
  // omit selection decorations (matches old behaviour).
599
611
  var statusDecorations = statusDecorationSet.find();
600
612
  if (statusDecorations.length === 0) {
601
- return hasFocus ? selectionDecorationSet : _view.DecorationSet.empty;
613
+ return hasFocus ? selectionDecorationSet : _view2.DecorationSet.empty;
602
614
  } else {
603
615
  return hasFocus ? selectionDecorationSet.add(state.doc, statusDecorations) : statusDecorationSet;
604
616
  }
@@ -618,22 +630,22 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
618
630
  var dragDecorations = [];
619
631
  state.doc.descendants(function (node, pos) {
620
632
  if (node.type.name === 'bodiedSyncBlock' && isOffline) {
621
- offlineDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
633
+ offlineDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
622
634
  class: _syncBlock.SyncBlockStateCssClassName.disabledClassName
623
635
  }));
624
636
  }
625
637
  if (_syncBlockStore.isSyncBlock(node) && isViewMode) {
626
- viewModeDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
638
+ viewModeDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
627
639
  class: _syncBlock.SyncBlockStateCssClassName.viewModeClassName
628
640
  }));
629
641
  }
630
642
  if (node.type.name === 'bodiedSyncBlock' && _syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
631
- loadingDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
643
+ loadingDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
632
644
  class: _syncBlock.SyncBlockStateCssClassName.creationLoadingClassName
633
645
  }));
634
646
  }
635
647
  if (isDragging && _syncBlockStore.isSyncBlock(node)) {
636
- dragDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
648
+ dragDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
637
649
  class: _syncBlock.SyncBlockStateCssClassName.draggingClassName
638
650
  }));
639
651
  }
@@ -641,7 +653,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
641
653
  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
654
  return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
643
655
  } else {
644
- return _view.DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
656
+ return _view2.DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
645
657
  }
646
658
  }
647
659
  },
@@ -662,6 +674,30 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
662
674
  return false;
663
675
  }
664
676
  },
677
+ transformPasted: function transformPasted(slice, _view) {
678
+ if (!(0, _platformFeatureFlags.fg)('platform_synced_block_patch_13')) {
679
+ return slice;
680
+ }
681
+
682
+ // Defense against bodiedSyncBlock nodes arriving via paste
683
+ // (e.g. drag-and-drop, cut-paste, or browser-level clipboard
684
+ // operations that bypass the transformCopied handler).
685
+ // We cannot convert to a syncBlock reference here because we don't
686
+ // know the source page's parentId (generateResourceIdForReference
687
+ // would use the current page's parentId which is wrong).
688
+ // Instead, strip the bodiedSyncBlock wrapper and keep only the
689
+ // inner content to prevent createBlock being called with the
690
+ // wrong parentId.
691
+ return (0, _utils.mapSlice)(slice, function (node) {
692
+ if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
693
+ // Return the inner content without the sync block wrapper.
694
+ // This is the same behavior as when a partial selection of
695
+ // a bodiedSyncBlock is copied (see transformCopied line 937).
696
+ return node.content;
697
+ }
698
+ return node;
699
+ });
700
+ },
665
701
  transformCopied: function transformCopied(slice, _ref9) {
666
702
  var state = _ref9.state;
667
703
  var pluginState = syncedBlockPluginKey.getState(state);
@@ -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
  }
@@ -630,6 +642,30 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
630
642
  return false;
631
643
  }
632
644
  },
645
+ transformPasted: (slice, _view) => {
646
+ if (!fg('platform_synced_block_patch_13')) {
647
+ return slice;
648
+ }
649
+
650
+ // Defense against bodiedSyncBlock nodes arriving via paste
651
+ // (e.g. drag-and-drop, cut-paste, or browser-level clipboard
652
+ // operations that bypass the transformCopied handler).
653
+ // We cannot convert to a syncBlock reference here because we don't
654
+ // know the source page's parentId (generateResourceIdForReference
655
+ // would use the current page's parentId which is wrong).
656
+ // Instead, strip the bodiedSyncBlock wrapper and keep only the
657
+ // inner content to prevent createBlock being called with the
658
+ // wrong parentId.
659
+ return mapSlice(slice, node => {
660
+ if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
661
+ // Return the inner content without the sync block wrapper.
662
+ // This is the same behavior as when a partial selection of
663
+ // a bodiedSyncBlock is copied (see transformCopied line 937).
664
+ return node.content;
665
+ }
666
+ return node;
667
+ });
668
+ },
633
669
  transformCopied: (slice, {
634
670
  state
635
671
  }) => {
@@ -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
  }
@@ -655,6 +667,30 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
655
667
  return false;
656
668
  }
657
669
  },
670
+ transformPasted: function transformPasted(slice, _view) {
671
+ if (!fg('platform_synced_block_patch_13')) {
672
+ return slice;
673
+ }
674
+
675
+ // Defense against bodiedSyncBlock nodes arriving via paste
676
+ // (e.g. drag-and-drop, cut-paste, or browser-level clipboard
677
+ // operations that bypass the transformCopied handler).
678
+ // We cannot convert to a syncBlock reference here because we don't
679
+ // know the source page's parentId (generateResourceIdForReference
680
+ // would use the current page's parentId which is wrong).
681
+ // Instead, strip the bodiedSyncBlock wrapper and keep only the
682
+ // inner content to prevent createBlock being called with the
683
+ // wrong parentId.
684
+ return mapSlice(slice, function (node) {
685
+ if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
686
+ // Return the inner content without the sync block wrapper.
687
+ // This is the same behavior as when a partial selection of
688
+ // a bodiedSyncBlock is copied (see transformCopied line 937).
689
+ return node.content;
690
+ }
691
+ return node;
692
+ });
693
+ },
658
694
  transformCopied: function transformCopied(slice, _ref9) {
659
695
  var state = _ref9.state;
660
696
  var pluginState = syncedBlockPluginKey.getState(state);
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.3",
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",