@atlaskit/editor-plugin-synced-block 5.3.33 → 5.3.35

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.
@@ -5,15 +5,17 @@ import ReactNodeView from '@atlaskit/editor-common/react-node-view';
5
5
  import { BodiedSyncBlockSharedCssClassName } from '@atlaskit/editor-common/sync-block';
6
6
  import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
7
7
  import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
8
+ import { fg } from '@atlaskit/platform-feature-flags';
8
9
  import { BodiedSyncBlockWrapper } from '../ui/BodiedSyncBlockWrapper';
9
10
  const toDOM = () => ['div', {
10
11
  class: BodiedSyncBlockSharedCssClassName.content,
11
12
  contenteditable: true
12
13
  }, 0];
13
- class BodiedSyncBlock extends ReactNodeView {
14
+ export class BodiedSyncBlock extends ReactNodeView {
14
15
  constructor(props) {
15
16
  super(props.node, props.view, props.getPos, props.portalProviderAPI, props.eventDispatcher, props);
16
17
  this.api = props.api;
18
+ this.syncBlockStore = props.syncBlockStore;
17
19
  this.handleConnectivityModeChange();
18
20
  this.handleViewModeChange();
19
21
  }
@@ -62,8 +64,9 @@ class BodiedSyncBlock extends ReactNodeView {
62
64
  return domRef;
63
65
  }
64
66
  render(_props, forwardRef) {
65
- var _this$api5, _this$api5$syncedBloc, _this$api5$syncedBloc2, _this$api6, _this$api6$analytics;
66
- const syncBlockStore = (_this$api5 = this.api) === null || _this$api5 === void 0 ? void 0 : (_this$api5$syncedBloc = _this$api5.syncedBlock.sharedState) === null || _this$api5$syncedBloc === void 0 ? void 0 : (_this$api5$syncedBloc2 = _this$api5$syncedBloc.currentState()) === null || _this$api5$syncedBloc2 === void 0 ? void 0 : _this$api5$syncedBloc2.syncBlockStore;
67
+ var _this$api$syncedBlock, _this$api5, _this$api5$syncedBloc, _this$api5$syncedBloc2, _this$api6, _this$api6$analytics;
68
+ // Use passed syncBlockStore for SSR where sharedState.currentState() is delayed
69
+ const syncBlockStore = (_this$api$syncedBlock = (_this$api5 = this.api) === null || _this$api5 === void 0 ? void 0 : (_this$api5$syncedBloc = _this$api5.syncedBlock.sharedState) === null || _this$api5$syncedBloc === void 0 ? void 0 : (_this$api5$syncedBloc2 = _this$api5$syncedBloc.currentState()) === null || _this$api5$syncedBloc2 === void 0 ? void 0 : _this$api5$syncedBloc2.syncBlockStore) !== null && _this$api$syncedBlock !== void 0 ? _this$api$syncedBlock : this.syncBlockStore;
67
70
  if (!syncBlockStore) {
68
71
  return null;
69
72
  }
@@ -82,12 +85,14 @@ class BodiedSyncBlock extends ReactNodeView {
82
85
  dom,
83
86
  contentDOM
84
87
  } = DOMSerializer.renderSpec(document, toDOM());
85
- if (dom instanceof HTMLElement) {
88
+ // In SSR, the first check won't work, so fallback to nodeType check
89
+ if (dom instanceof HTMLElement || dom.nodeType === 1 && fg('platform_synced_block_patch_5')) {
86
90
  this.updateContentEditable({
87
91
  contentDOM
88
92
  });
93
+ // eslint-disable-next-line @atlaskit/editor/no-as-casting
89
94
  return {
90
- dom,
95
+ dom: dom,
91
96
  contentDOM
92
97
  };
93
98
  }
@@ -105,7 +110,8 @@ class BodiedSyncBlock extends ReactNodeView {
105
110
  export const bodiedSyncBlockNodeView = ({
106
111
  pluginOptions,
107
112
  pmPluginFactoryParams,
108
- api
113
+ api,
114
+ syncBlockStore
109
115
  }) => (node, view, getPos) => {
110
116
  const {
111
117
  portalProviderAPI,
@@ -118,6 +124,7 @@ export const bodiedSyncBlockNodeView = ({
118
124
  view,
119
125
  getPos,
120
126
  portalProviderAPI,
121
- eventDispatcher
127
+ eventDispatcher,
128
+ syncBlockStore
122
129
  }).init();
123
130
  };
@@ -1,3 +1,4 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
1
2
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
2
3
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
4
  import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
@@ -9,6 +10,7 @@ import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
9
10
  import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
10
11
  import { fg } from '@atlaskit/platform-feature-flags';
11
12
  import { lazyBodiedSyncBlockView } from '../nodeviews/bodiedLazySyncedBlock';
13
+ import { bodiedSyncBlockNodeView } from '../nodeviews/bodiedSyncedBlock';
12
14
  import { SyncBlock as SyncBlockView } from '../nodeviews/syncedBlock';
13
15
  import { FLAG_ID } from '../types';
14
16
  import { handleBodiedSyncBlockCreation } from './utils/handle-bodied-sync-block-creation';
@@ -16,13 +18,15 @@ import { handleBodiedSyncBlockRemoval } from './utils/handle-bodied-sync-block-r
16
18
  import { shouldIgnoreDomEvent } from './utils/ignore-dom-event';
17
19
  import { calculateDecorations } from './utils/selection-decorations';
18
20
  import { hasEditInSyncBlock, trackSyncBlocks } from './utils/track-sync-blocks';
19
- import { wasInlineExtensionInsertedInBodiedSyncBlock, sliceFullyContainsNode } from './utils/utils';
21
+ import { deferDispatch, wasInlineExtensionInsertedInBodiedSyncBlock, sliceFullyContainsNode } from './utils/utils';
20
22
  export const syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
21
23
  const mapRetryCreationPosMap = (oldMap, newRetryCreationPos, mapPos) => {
22
24
  const resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;
23
25
  const newMap = new Map(oldMap);
24
26
  if (resourceId) {
25
- const pos = newRetryCreationPos.pos;
27
+ const {
28
+ pos
29
+ } = newRetryCreationPos;
26
30
  if (!pos) {
27
31
  newMap.delete(resourceId);
28
32
  } else {
@@ -41,18 +45,15 @@ const mapRetryCreationPosMap = (oldMap, newRetryCreationPos, mapPos) => {
41
45
  return newMap;
42
46
  };
43
47
  const showCopiedFlag = api => {
44
- // Use setTimeout to dispatch transaction in next tick and avoid re-entrant dispatch
45
- setTimeout(() => {
48
+ deferDispatch(() => {
46
49
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
47
50
  tr
48
- }) => {
49
- return tr.setMeta(syncedBlockPluginKey, {
50
- activeFlag: {
51
- id: FLAG_ID.SYNC_BLOCK_COPIED
52
- }
53
- });
54
- });
55
- }, 0);
51
+ }) => tr.setMeta(syncedBlockPluginKey, {
52
+ activeFlag: {
53
+ id: FLAG_ID.SYNC_BLOCK_COPIED
54
+ }
55
+ }));
56
+ });
56
57
  };
57
58
  const showInlineExtensionInSyncBlockWarningIfNeeded = (tr, state, api, inlineExtensionFlagShown) => {
58
59
  var _api$connectivity, _api$connectivity$sha;
@@ -66,18 +67,15 @@ const showInlineExtensionInSyncBlockWarningIfNeeded = (tr, state, api, inlineExt
66
67
  // Only show the flag on the first instance per sync block (same as UNPUBLISHED_SYNC_BLOCK_PASTED)
67
68
  if (resourceId && !inlineExtensionFlagShown.has(resourceId)) {
68
69
  inlineExtensionFlagShown.add(resourceId);
69
- // Use setTimeout to dispatch in next tick and avoid re-entrant dispatch from filterTransaction
70
- setTimeout(() => {
70
+ deferDispatch(() => {
71
71
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
72
72
  tr
73
- }) => {
74
- return tr.setMeta(syncedBlockPluginKey, {
75
- activeFlag: {
76
- id: FLAG_ID.INLINE_EXTENSION_IN_SYNC_BLOCK
77
- }
78
- });
79
- });
80
- }, 0);
73
+ }) => tr.setMeta(syncedBlockPluginKey, {
74
+ activeFlag: {
75
+ id: FLAG_ID.INLINE_EXTENSION_IN_SYNC_BLOCK
76
+ }
77
+ }));
78
+ });
81
79
  }
82
80
  };
83
81
  const getDeleteReason = tr => {
@@ -87,37 +85,166 @@ const getDeleteReason = tr => {
87
85
  }
88
86
  return reason;
89
87
  };
88
+ const filterTransactionOnline = ({
89
+ tr,
90
+ state,
91
+ syncBlockStore,
92
+ api,
93
+ confirmationTransactionRef,
94
+ bodiedSyncBlockRemoved,
95
+ bodiedSyncBlockAdded,
96
+ inlineExtensionFlagShown
97
+ }) => {
98
+ const {
99
+ removed: syncBlockRemoved,
100
+ added: syncBlockAdded
101
+ } = trackSyncBlocks(node => node.type.name === 'syncBlock', tr, state);
102
+ syncBlockRemoved.forEach(syncBlock => {
103
+ var _api$analytics, _api$analytics$action;
104
+ api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent({
105
+ action: ACTION.DELETED,
106
+ actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
107
+ actionSubjectId: ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_DELETE,
108
+ attributes: {
109
+ resourceId: syncBlock.attrs.resourceId,
110
+ blockInstanceId: syncBlock.attrs.localId
111
+ },
112
+ eventType: EVENT_TYPE.OPERATIONAL
113
+ });
114
+ });
115
+ syncBlockAdded.forEach(syncBlock => {
116
+ if (fg('platform_synced_block_patch_3')) {
117
+ var _api$analytics2, _api$analytics2$actio;
118
+ 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({
119
+ action: ACTION.INSERTED,
120
+ actionSubject: ACTION_SUBJECT.DOCUMENT,
121
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK,
122
+ attributes: {
123
+ resourceId: syncBlock.attrs.resourceId,
124
+ blockInstanceId: syncBlock.attrs.localId
125
+ },
126
+ eventType: EVENT_TYPE.TRACK
127
+ });
128
+ } else {
129
+ var _api$analytics3, _api$analytics3$actio;
130
+ api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : (_api$analytics3$actio = _api$analytics3.actions) === null || _api$analytics3$actio === void 0 ? void 0 : _api$analytics3$actio.fireAnalyticsEvent({
131
+ action: ACTION.INSERTED,
132
+ actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
133
+ actionSubjectId: ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_CREATE,
134
+ attributes: {
135
+ resourceId: syncBlock.attrs.resourceId,
136
+ blockInstanceId: syncBlock.attrs.localId
137
+ },
138
+ eventType: EVENT_TYPE.OPERATIONAL
139
+ });
140
+ }
141
+ });
142
+ if (bodiedSyncBlockRemoved.length > 0) {
143
+ // eslint-disable-next-line no-param-reassign
144
+ confirmationTransactionRef.current = tr;
145
+ return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
146
+ }
147
+ if (bodiedSyncBlockAdded.length > 0) {
148
+ if (tr.getMeta(pmHistoryPluginKey)) {
149
+ // We don't allow bodiedSyncBlock creation via redo, however, we need to return true here to let transaction through so history can be updated properly.
150
+ // If we simply returns false, creation from redo is blocked as desired, but this results in editor showing redo as possible even though it's not.
151
+ // After true is returned here and the node is created, we delete the node in the filterTransaction immediately, which cancels out the creation
152
+ return true;
153
+ }
154
+ handleBodiedSyncBlockCreation(bodiedSyncBlockAdded, state, api);
155
+ return true;
156
+ }
157
+ showInlineExtensionInSyncBlockWarningIfNeeded(tr, state, api, inlineExtensionFlagShown);
158
+ return true;
159
+ };
160
+ const filterTransactionOffline = ({
161
+ tr,
162
+ state,
163
+ api,
164
+ isConfirmedSyncBlockDeletion,
165
+ bodiedSyncBlockRemoved,
166
+ bodiedSyncBlockAdded
167
+ }) => {
168
+ const {
169
+ removed: syncBlockRemoved,
170
+ added: syncBlockAdded
171
+ } = trackSyncBlocks(node => node.type.name === 'syncBlock', tr, state);
172
+ let errorFlag = false;
173
+ if (isConfirmedSyncBlockDeletion || bodiedSyncBlockRemoved.length > 0 || syncBlockRemoved.length > 0) {
174
+ errorFlag = FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE;
175
+ } else if (bodiedSyncBlockAdded.length > 0 || syncBlockAdded.length > 0) {
176
+ errorFlag = FLAG_ID.CANNOT_CREATE_WHEN_OFFLINE;
177
+ } else if (hasEditInSyncBlock(tr, state)) {
178
+ errorFlag = FLAG_ID.CANNOT_EDIT_WHEN_OFFLINE;
179
+ }
180
+ if (errorFlag) {
181
+ deferDispatch(() => {
182
+ api === null || api === void 0 ? void 0 : api.core.actions.execute(({
183
+ tr
184
+ }) => tr.setMeta(syncedBlockPluginKey, {
185
+ activeFlag: {
186
+ id: errorFlag
187
+ }
188
+ }));
189
+ });
190
+ return false;
191
+ }
192
+ return true;
193
+ };
194
+
195
+ /**
196
+ * Encapsulates mutable state that persists across transactions in the
197
+ * synced block plugin. Replaces module-level closure variables so state
198
+ * is explicitly scoped to a single plugin instance.
199
+ */
200
+ class SyncedBlockPluginContext {
201
+ constructor() {
202
+ _defineProperty(this, "confirmationTransactionRef", {
203
+ current: undefined
204
+ });
205
+ _defineProperty(this, "_isCopyEvent", false);
206
+ _defineProperty(this, "unpublishedFlagShown", new Set());
207
+ _defineProperty(this, "inlineExtensionFlagShown", new Set());
208
+ }
209
+ get isCopyEvent() {
210
+ return this._isCopyEvent;
211
+ }
212
+ markCopyEvent() {
213
+ this._isCopyEvent = true;
214
+ }
215
+ consumeCopyEvent() {
216
+ const was = this._isCopyEvent;
217
+ this._isCopyEvent = false;
218
+ return was;
219
+ }
220
+ }
90
221
  export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api) => {
222
+ var _ctx$confirmationTran, _ctx$unpublishedFlagS, _ctx$inlineExtensionF;
91
223
  const {
92
224
  useLongPressSelection = false
93
225
  } = options || {};
94
- const confirmationTransactionRef = {
226
+ const ctx = fg('platform_synced_block_patch_5') ? new SyncedBlockPluginContext() : undefined;
227
+ const confirmationTransactionRef = (_ctx$confirmationTran = ctx === null || ctx === void 0 ? void 0 : ctx.confirmationTransactionRef) !== null && _ctx$confirmationTran !== void 0 ? _ctx$confirmationTran : {
95
228
  current: undefined
96
229
  };
97
- // Track if a copy event occurred to distinguish copy from drag and drop
98
230
  let isCopyEvent = false;
99
- // Track which sync blocks have already triggered the unpublished flag
100
- const unpublishedFlagShown = new Set();
101
- // Track which sync blocks have already triggered the inline extension in sync block flag
102
- const inlineExtensionFlagShown = new Set();
231
+ const unpublishedFlagShown = (_ctx$unpublishedFlagS = ctx === null || ctx === void 0 ? void 0 : ctx.unpublishedFlagShown) !== null && _ctx$unpublishedFlagS !== void 0 ? _ctx$unpublishedFlagS : new Set();
232
+ const inlineExtensionFlagShown = (_ctx$inlineExtensionF = ctx === null || ctx === void 0 ? void 0 : ctx.inlineExtensionFlagShown) !== null && _ctx$inlineExtensionF !== void 0 ? _ctx$inlineExtensionF : new Set();
103
233
 
104
234
  // Set up callback to detect unpublished sync blocks when they're fetched
105
235
  syncBlockStore.referenceManager.setOnUnpublishedSyncBlockDetected(resourceId => {
106
236
  // Only show the flag once per sync block
107
237
  if (!unpublishedFlagShown.has(resourceId)) {
108
238
  unpublishedFlagShown.add(resourceId);
109
- // Use setTimeout to dispatch transaction in next tick and avoid re-entrant dispatch
110
- setTimeout(() => {
239
+ deferDispatch(() => {
111
240
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
112
241
  tr
113
- }) => {
114
- return tr.setMeta(syncedBlockPluginKey, {
115
- activeFlag: {
116
- id: FLAG_ID.UNPUBLISHED_SYNC_BLOCK_PASTED
117
- }
118
- });
119
- });
120
- }, 0);
242
+ }) => tr.setMeta(syncedBlockPluginKey, {
243
+ activeFlag: {
244
+ id: FLAG_ID.UNPUBLISHED_SYNC_BLOCK_PASTED
245
+ }
246
+ }));
247
+ });
121
248
  }
122
249
  });
123
250
  return new SafePlugin({
@@ -159,23 +286,27 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
159
286
  },
160
287
  props: {
161
288
  nodeViews: {
162
- syncBlock: (node, view, getPos, _decorations) => {
163
- // To support SSR, pass `syncBlockStore` here
164
- // and do not use lazy loading.
165
- // We cannot start rendering and then load `syncBlockStore` asynchronously,
166
- // because obtaining it is asynchronous (sharedPluginState.currentState() is delayed).
167
- return new SyncBlockView({
168
- api,
169
- options,
170
- node,
171
- view,
172
- getPos,
173
- portalProviderAPI: pmPluginFactoryParams.portalProviderAPI,
174
- eventDispatcher: pmPluginFactoryParams.eventDispatcher,
175
- syncBlockStore: syncBlockStore
176
- }).init();
177
- },
178
- bodiedSyncBlock: lazyBodiedSyncBlockView({
289
+ syncBlock: (node, view, getPos, _decorations) =>
290
+ // To support SSR, pass `syncBlockStore` here
291
+ // and do not use lazy loading.
292
+ // We cannot start rendering and then load `syncBlockStore` asynchronously,
293
+ // because obtaining it is asynchronous (sharedPluginState.currentState() is delayed).
294
+ new SyncBlockView({
295
+ api,
296
+ options,
297
+ node,
298
+ view,
299
+ getPos,
300
+ portalProviderAPI: pmPluginFactoryParams.portalProviderAPI,
301
+ eventDispatcher: pmPluginFactoryParams.eventDispatcher,
302
+ syncBlockStore: syncBlockStore
303
+ }).init(),
304
+ bodiedSyncBlock: fg('platform_synced_block_patch_5') ? bodiedSyncBlockNodeView({
305
+ pluginOptions: options,
306
+ pmPluginFactoryParams,
307
+ api,
308
+ syncBlockStore
309
+ }) : lazyBodiedSyncBlockView({
179
310
  pluginOptions: options,
180
311
  pmPluginFactoryParams,
181
312
  api
@@ -224,7 +355,11 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
224
355
  return shouldIgnoreDomEvent(view, event, api);
225
356
  },
226
357
  copy: () => {
227
- isCopyEvent = true;
358
+ if (ctx) {
359
+ ctx.markCopyEvent();
360
+ } else {
361
+ isCopyEvent = true;
362
+ }
228
363
  return false;
229
364
  }
230
365
  },
@@ -236,8 +371,10 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
236
371
  const {
237
372
  schema
238
373
  } = state;
239
- const isCopy = isCopyEvent;
240
- isCopyEvent = false;
374
+ const isCopy = ctx ? ctx.consumeCopyEvent() : isCopyEvent;
375
+ if (!ctx) {
376
+ isCopyEvent = false;
377
+ }
241
378
  if (!syncBlockStore || !isCopy) {
242
379
  return slice;
243
380
  }
@@ -280,7 +417,6 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
280
417
  const {
281
418
  added
282
419
  } = trackSyncBlocks(node => node.type.name === 'syncBlock', tr, state);
283
- // Mark newly added sync blocks so we can detect unpublished status when data is fetched
284
420
  added.forEach(nodeInfo => {
285
421
  var _nodeInfo$attrs;
286
422
  if ((_nodeInfo$attrs = nodeInfo.attrs) !== null && _nodeInfo$attrs !== void 0 && _nodeInfo$attrs.resourceId) {
@@ -288,11 +424,6 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
288
424
  }
289
425
  });
290
426
  }
291
-
292
- // Ignore transactions that don't change the document
293
- // or are from remote (collab) or already confirmed sync block deletion
294
- // We only care about local changes that change the document
295
- // and are not yet confirmed for sync block deletion
296
427
  if (!tr.docChanged || Boolean(tr.getMeta('isRemote')) || !isOffline && isConfirmedSyncBlockDeletion) {
297
428
  return true;
298
429
  }
@@ -300,14 +431,33 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
300
431
  removed: bodiedSyncBlockRemoved,
301
432
  added: bodiedSyncBlockAdded
302
433
  } = trackSyncBlocks(syncBlockStore.sourceManager.isSourceBlock, tr, state);
434
+ if (fg('platform_synced_block_patch_5')) {
435
+ return isOffline ? filterTransactionOffline({
436
+ tr,
437
+ state,
438
+ api,
439
+ isConfirmedSyncBlockDeletion,
440
+ bodiedSyncBlockRemoved,
441
+ bodiedSyncBlockAdded
442
+ }) : filterTransactionOnline({
443
+ tr,
444
+ state,
445
+ syncBlockStore,
446
+ api,
447
+ confirmationTransactionRef,
448
+ bodiedSyncBlockRemoved,
449
+ bodiedSyncBlockAdded,
450
+ inlineExtensionFlagShown
451
+ });
452
+ }
303
453
  if (!isOffline) {
304
454
  const {
305
455
  removed: syncBlockRemoved,
306
456
  added: syncBlockAdded
307
457
  } = trackSyncBlocks(node => node.type.name === 'syncBlock', tr, state);
308
458
  syncBlockRemoved.forEach(syncBlock => {
309
- var _api$analytics, _api$analytics$action;
310
- api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent({
459
+ var _api$analytics4, _api$analytics4$actio;
460
+ api === null || api === void 0 ? void 0 : (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : (_api$analytics4$actio = _api$analytics4.actions) === null || _api$analytics4$actio === void 0 ? void 0 : _api$analytics4$actio.fireAnalyticsEvent({
311
461
  action: ACTION.DELETED,
312
462
  actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
313
463
  actionSubjectId: ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_DELETE,
@@ -320,8 +470,8 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
320
470
  });
321
471
  syncBlockAdded.forEach(syncBlock => {
322
472
  if (fg('platform_synced_block_patch_3')) {
323
- var _api$analytics2, _api$analytics2$actio;
324
- 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({
473
+ var _api$analytics5, _api$analytics5$actio;
474
+ api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : (_api$analytics5$actio = _api$analytics5.actions) === null || _api$analytics5$actio === void 0 ? void 0 : _api$analytics5$actio.fireAnalyticsEvent({
325
475
  action: ACTION.INSERTED,
326
476
  actionSubject: ACTION_SUBJECT.DOCUMENT,
327
477
  actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK,
@@ -332,8 +482,8 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
332
482
  eventType: EVENT_TYPE.TRACK
333
483
  });
334
484
  } else {
335
- var _api$analytics3, _api$analytics3$actio;
336
- api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : (_api$analytics3$actio = _api$analytics3.actions) === null || _api$analytics3$actio === void 0 ? void 0 : _api$analytics3$actio.fireAnalyticsEvent({
485
+ var _api$analytics6, _api$analytics6$actio;
486
+ api === null || api === void 0 ? void 0 : (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 ? void 0 : (_api$analytics6$actio = _api$analytics6.actions) === null || _api$analytics6$actio === void 0 ? void 0 : _api$analytics6$actio.fireAnalyticsEvent({
337
487
  action: ACTION.INSERTED,
338
488
  actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
339
489
  actionSubjectId: ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_CREATE,
@@ -350,7 +500,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
350
500
  return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
351
501
  }
352
502
  if (bodiedSyncBlockAdded.length > 0) {
353
- if (Boolean(tr.getMeta(pmHistoryPluginKey))) {
503
+ if (tr.getMeta(pmHistoryPluginKey)) {
354
504
  // We don't allow bodiedSyncBlock creation via redo, however, we need to return true here to let transaction through so history can be updated properly.
355
505
  // If we simply returns false, creation from redo is blocked as desired, but this results in editor showing redo as possible even though it's not.
356
506
  // After true is returned here and the node is created, we delete the node in the filterTransaction immediately, which cancels out the creation
@@ -361,36 +511,32 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
361
511
  }
362
512
  showInlineExtensionInSyncBlockWarningIfNeeded(tr, state, api, inlineExtensionFlagShown);
363
513
  return true;
364
- } else {
365
- const {
366
- removed: syncBlockRemoved,
367
- added: syncBlockAdded
368
- } = trackSyncBlocks(node => node.type.name === 'syncBlock', tr, state);
369
- let errorFlag = false;
514
+ }
515
+ const {
516
+ removed: syncBlockRemoved,
517
+ added: syncBlockAdded
518
+ } = trackSyncBlocks(node => node.type.name === 'syncBlock', tr, state);
519
+ let errorFlag = false;
370
520
 
371
- // Disable (bodied)syncBlock node deletion/creation/edition in offline mode and trigger an error flag instead
372
- if (isConfirmedSyncBlockDeletion || bodiedSyncBlockRemoved.length > 0 || syncBlockRemoved.length > 0) {
373
- errorFlag = FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE;
374
- } else if (bodiedSyncBlockAdded.length > 0 || syncBlockAdded.length > 0) {
375
- errorFlag = FLAG_ID.CANNOT_CREATE_WHEN_OFFLINE;
376
- } else if (hasEditInSyncBlock(tr, state)) {
377
- errorFlag = FLAG_ID.CANNOT_EDIT_WHEN_OFFLINE;
378
- }
379
- if (errorFlag) {
380
- // Use setTimeout to dispatch transaction in next tick and avoid re-entrant dispatch
381
- setTimeout(() => {
382
- api === null || api === void 0 ? void 0 : api.core.actions.execute(({
383
- tr
384
- }) => {
385
- return tr.setMeta(syncedBlockPluginKey, {
386
- activeFlag: {
387
- id: errorFlag
388
- }
389
- });
390
- });
391
- }, 0);
392
- return false;
393
- }
521
+ // Disable (bodied)syncBlock node deletion/creation/edition in offline mode and trigger an error flag instead
522
+ if (isConfirmedSyncBlockDeletion || bodiedSyncBlockRemoved.length > 0 || syncBlockRemoved.length > 0) {
523
+ errorFlag = FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE;
524
+ } else if (bodiedSyncBlockAdded.length > 0 || syncBlockAdded.length > 0) {
525
+ errorFlag = FLAG_ID.CANNOT_CREATE_WHEN_OFFLINE;
526
+ } else if (hasEditInSyncBlock(tr, state)) {
527
+ errorFlag = FLAG_ID.CANNOT_EDIT_WHEN_OFFLINE;
528
+ }
529
+ if (errorFlag) {
530
+ deferDispatch(() => {
531
+ api === null || api === void 0 ? void 0 : api.core.actions.execute(({
532
+ tr
533
+ }) => tr.setMeta(syncedBlockPluginKey, {
534
+ activeFlag: {
535
+ id: errorFlag
536
+ }
537
+ }));
538
+ });
539
+ return false;
394
540
  }
395
541
  return true;
396
542
  },
@@ -401,22 +547,23 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
401
547
  }
402
548
  });
403
549
  for (const tr of trs) {
404
- if (!tr.getMeta(pmHistoryPluginKey)) {
405
- continue;
406
- }
407
- const {
408
- added
409
- } = trackSyncBlocks(syncBlockStore.sourceManager.isSourceBlock, tr, oldState);
410
- if (added.length > 0) {
411
- // Delete bodiedSyncBlock if it's originated from history, i.e. redo creation
412
- // See filterTransaction above for more details
413
- const tr = newState.tr;
414
- added.forEach(node => {
415
- if (node.from !== undefined && node.to !== undefined) {
416
- tr.delete(node.from, node.to);
417
- }
418
- });
419
- return tr;
550
+ if (tr.getMeta(pmHistoryPluginKey)) {
551
+ const {
552
+ added
553
+ } = trackSyncBlocks(syncBlockStore.sourceManager.isSourceBlock, tr, oldState);
554
+ if (added.length > 0) {
555
+ // Delete bodiedSyncBlock if it's originated from history, i.e. redo creation
556
+ // See filterTransaction above for more details
557
+ const {
558
+ tr
559
+ } = newState;
560
+ added.forEach(node => {
561
+ if (node.from !== undefined && node.to !== undefined) {
562
+ tr.delete(node.from, node.to);
563
+ }
564
+ });
565
+ return tr;
566
+ }
420
567
  }
421
568
  }
422
569
  return null;
@@ -1,6 +1,7 @@
1
1
  import { TextSelection } from '@atlaskit/editor-prosemirror/state';
2
2
  import { FLAG_ID } from '../../types';
3
3
  import { syncedBlockPluginKey } from '../main';
4
+ import { deferDispatch } from './utils';
4
5
  const onRetry = (api, resourceId) => {
5
6
  return () => {
6
7
  var _api$core, _api$core2;
@@ -77,7 +78,7 @@ export const handleBodiedSyncBlockCreation = (bodiedSyncBlockAdded, editorState,
77
78
  to: node.to
78
79
  };
79
80
  const resourceId = node.attrs.resourceId;
80
- setTimeout(() => {
81
+ deferDispatch(() => {
81
82
  var _api$core3;
82
83
  api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(({
83
84
  tr
@@ -2,6 +2,19 @@ import { expandSelectionToBlockRange } from '@atlaskit/editor-common/selection';
2
2
  import { Fragment } from '@atlaskit/editor-prosemirror/model';
3
3
  import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
4
4
  import { findParentNodeOfType, findParentNodeOfTypeClosestToPos, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
5
+ import { fg } from '@atlaskit/platform-feature-flags';
6
+
7
+ /**
8
+ * Defers a callback to the next microtask (when gated) or next macrotask via setTimeout(0).
9
+ * Used to avoid re-entrant ProseMirror dispatch cycles.
10
+ */
11
+ export const deferDispatch = fn => {
12
+ if (fg('platform_synced_block_patch_5')) {
13
+ queueMicrotask(fn);
14
+ } else {
15
+ setTimeout(fn, 0);
16
+ }
17
+ };
5
18
  export const findSyncBlock = (schema, selection) => {
6
19
  const {
7
20
  syncBlock
@@ -5,7 +5,7 @@ import { DOMSerializer, Fragment } from '@atlaskit/editor-prosemirror/model';
5
5
  import { TextSelection } from '@atlaskit/editor-prosemirror/state';
6
6
  import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
7
7
  import { syncedBlockPluginKey } from '../pm-plugins/main';
8
- import { canBeConvertedToSyncBlock, findSyncBlock, findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
8
+ import { canBeConvertedToSyncBlock, deferDispatch, findSyncBlock, findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
9
9
  import { FLAG_ID } from '../types';
10
10
  import { pasteSyncBlockHTMLContent } from './utils';
11
11
  export var createSyncedBlock = function createSyncedBlock(_ref) {
@@ -147,9 +147,7 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
147
147
  }
148
148
  var domNode = toDOM(referenceSyncBlockNode, schema);
149
149
  copyDomNode(domNode, referenceSyncBlockNode.type, selection);
150
-
151
- // Use setTimeout to dispatch transaction in next tick and avoid re-entrant dispatch
152
- setTimeout(function () {
150
+ deferDispatch(function () {
153
151
  api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
154
152
  var _api$analytics4;
155
153
  var tr = _ref3.tr;
@@ -169,7 +167,7 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
169
167
  }
170
168
  });
171
169
  });
172
- }, 0);
170
+ });
173
171
  return true;
174
172
  };
175
173
  export var editSyncedBlockSource = function editSyncedBlockSource(syncBlockStore, api) {