@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 +2 -2
- package/CHANGELOG.md +30 -0
- package/dist/cjs/editor-commands/index.js +12 -1
- package/dist/cjs/pm-plugins/main.js +215 -25
- package/dist/cjs/pm-plugins/utils/handle-bodied-sync-block-removal.js +14 -2
- package/dist/cjs/pm-plugins/utils/recompute-delete-transaction.js +66 -1
- package/dist/cjs/ui/floating-toolbar.js +3 -2
- package/dist/es2019/editor-commands/index.js +12 -1
- package/dist/es2019/pm-plugins/main.js +178 -11
- package/dist/es2019/pm-plugins/utils/handle-bodied-sync-block-removal.js +13 -3
- package/dist/es2019/pm-plugins/utils/recompute-delete-transaction.js +52 -0
- package/dist/es2019/ui/floating-toolbar.js +3 -2
- package/dist/esm/editor-commands/index.js +12 -1
- package/dist/esm/pm-plugins/main.js +214 -24
- package/dist/esm/pm-plugins/utils/handle-bodied-sync-block-removal.js +15 -3
- package/dist/esm/pm-plugins/utils/recompute-delete-transaction.js +65 -0
- package/dist/esm/ui/floating-toolbar.js +3 -2
- package/dist/types/pm-plugins/main.d.ts +2 -1
- package/dist/types/pm-plugins/utils/handle-bodied-sync-block-removal.d.ts +7 -1
- package/dist/types/pm-plugins/utils/recompute-delete-transaction.d.ts +29 -0
- package/dist/types/syncedBlockPluginType.d.ts +2 -2
- package/package.json +6 -3
- package/types/package.json +8 -0
|
@@ -39,6 +39,7 @@ var getToolbarConfig = exports.getToolbarConfig = function getToolbarConfig(stat
|
|
|
39
39
|
if (syncBlockStore.sourceManager.isPendingCreation(syncBlockObject.node.attrs.resourceId)) {
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
+
var isSyncBlockActivationEnabled = (0, _expValEquals.expValEquals)('platform_editor_sync_block_activation', 'isEnabled', true);
|
|
42
43
|
var syncBlockInstance = syncBlockStore.referenceManager.getFromCache(syncBlockObject.node.attrs.resourceId);
|
|
43
44
|
var isUnsyncedBlock = (syncBlockInstance === null || syncBlockInstance === void 0 || (_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.type) === _editorSyncedBlockProvider.SyncBlockError.NotFound;
|
|
44
45
|
var isErroredBlock = syncBlockInstance === null || syncBlockInstance === void 0 ? void 0 : syncBlockInstance.error;
|
|
@@ -59,7 +60,6 @@ var getToolbarConfig = exports.getToolbarConfig = function getToolbarConfig(stat
|
|
|
59
60
|
};
|
|
60
61
|
};
|
|
61
62
|
var items = [];
|
|
62
|
-
var isSyncBlockActivationEnabled = (0, _expValEquals.expValEquals)('platform_editor_sync_block_activation', 'isEnabled', true);
|
|
63
63
|
if (isUnsyncedBlock) {
|
|
64
64
|
var deleteButton = _objectSpread({
|
|
65
65
|
type: 'button',
|
|
@@ -197,7 +197,8 @@ var getToolbarConfig = exports.getToolbarConfig = function getToolbarConfig(stat
|
|
|
197
197
|
return tr.setSelection(new _selection.GapCursorSelection(tr.doc.resolve(syncBlockObject.pos + syncBlockObject.node.nodeSize), _selection.Side.RIGHT));
|
|
198
198
|
});
|
|
199
199
|
onGiveFeedback({
|
|
200
|
-
blockType: isBodiedSyncBlock ? 'source' : 'reference'
|
|
200
|
+
blockType: isBodiedSyncBlock ? 'source' : 'reference',
|
|
201
|
+
entryPoint: 'overflow-menu'
|
|
201
202
|
});
|
|
202
203
|
return true;
|
|
203
204
|
}
|
|
@@ -6,6 +6,7 @@ import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosem
|
|
|
6
6
|
import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
|
|
7
7
|
import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
|
|
8
8
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
9
|
+
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
9
10
|
import { creationMetaKey, deleteMechanismMetaKey, syncedBlockPluginKey } from '../pm-plugins/main';
|
|
10
11
|
import { canBeConvertedToSyncBlock, deferDispatch, findSyncBlock, findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
|
|
11
12
|
import { FLAG_ID } from '../types';
|
|
@@ -355,7 +356,11 @@ export const removeSyncedBlockAtPos = (api, pos) => {
|
|
|
355
356
|
const 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
|
-
|
|
359
|
+
const 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 (expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true)) {
|
|
361
|
+
removeTr.setMeta(deleteMechanismMetaKey, 'deleteButton');
|
|
362
|
+
}
|
|
363
|
+
return removeTr;
|
|
359
364
|
}
|
|
360
365
|
return tr;
|
|
361
366
|
});
|
|
@@ -376,6 +381,12 @@ export const unsync = (storeManager, isBodiedSyncBlock, view) => {
|
|
|
376
381
|
return false;
|
|
377
382
|
}
|
|
378
383
|
if (isBodiedSyncBlock) {
|
|
384
|
+
// Signal the unsync intent. This transaction is intercepted by the plugin's
|
|
385
|
+
// filterTransaction, which shows the deletion-confirmation modal and, on confirm,
|
|
386
|
+
// recomputes the actual document change from the live document keyed off the
|
|
387
|
+
// `deletionReason: 'source-block-unsynced'` meta (see handleBodiedSyncBlockRemoval /
|
|
388
|
+
// recomputeUnsyncTransaction). The real unwrap-vs-delete decision therefore lives in the
|
|
389
|
+
// confirm path, not here (EDITOR-8230).
|
|
379
390
|
const content = syncBlock === null || syncBlock === void 0 ? void 0 : syncBlock.node.content;
|
|
380
391
|
const {
|
|
381
392
|
tr
|
|
@@ -13,6 +13,7 @@ import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/edi
|
|
|
13
13
|
import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
|
|
14
14
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
15
15
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
16
|
+
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
16
17
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
17
18
|
import { bodiedSyncBlockNodeView, bodiedSyncBlockNodeViewOld } from '../nodeviews/bodiedSyncedBlock';
|
|
18
19
|
import { SyncBlock as SyncBlockView } from '../nodeviews/syncedBlock';
|
|
@@ -37,6 +38,7 @@ export const syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
|
|
|
37
38
|
* `platform_editor_blocks_patch_4`.
|
|
38
39
|
*/
|
|
39
40
|
export const deleteMechanismMetaKey = new PluginKey('syncedBlockDeleteMechanism');
|
|
41
|
+
const syncedBlockPromptedFeedbackMetaKey = new PluginKey('syncedBlockPromptedFeedback');
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* Creation-type signals set by {@link createSyncedBlock} on the creating
|
|
@@ -121,7 +123,7 @@ const getDeleteReason = tr => {
|
|
|
121
123
|
};
|
|
122
124
|
|
|
123
125
|
/** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
|
|
124
|
-
const isHistoryMeta = meta => typeof meta === 'object' && meta !== null && 'redo' in meta;
|
|
126
|
+
const isHistoryMeta = meta => typeof meta === 'object' && meta !== null && 'redo' in meta && typeof meta.redo === 'boolean';
|
|
125
127
|
|
|
126
128
|
/**
|
|
127
129
|
* Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
|
|
@@ -145,9 +147,16 @@ const isHistoryMeta = meta => typeof meta === 'object' && meta !== null && 'redo
|
|
|
145
147
|
* selected when the edit was made.
|
|
146
148
|
*/
|
|
147
149
|
export const getDeleteMechanism = (tr, state) => {
|
|
148
|
-
var _selection$node;
|
|
150
|
+
var _selection$node, _selection$node2;
|
|
151
|
+
// Keep the legacy analytics classification unchanged while activation is off.
|
|
152
|
+
// The stricter history, structural-step, and reference-selection handling is
|
|
153
|
+
// only needed by the prompted feedback rollout.
|
|
154
|
+
const isSyncBlockActivationEnabled = expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true);
|
|
149
155
|
const historyMeta = tr.getMeta(pmHistoryPluginKey);
|
|
150
156
|
if (historyMeta) {
|
|
157
|
+
if (isSyncBlockActivationEnabled && !isHistoryMeta(historyMeta)) {
|
|
158
|
+
return 'other';
|
|
159
|
+
}
|
|
151
160
|
return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
|
|
152
161
|
}
|
|
153
162
|
|
|
@@ -156,16 +165,26 @@ export const getDeleteMechanism = (tr, state) => {
|
|
|
156
165
|
if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
|
|
157
166
|
return 'deleteButton';
|
|
158
167
|
}
|
|
168
|
+
const hasStructuralStep = isSyncBlockActivationEnabled && tr.steps.some(step => step instanceof ReplaceAroundStep);
|
|
159
169
|
const hasReplaceStep = tr.steps.some(step => step instanceof ReplaceStep);
|
|
160
|
-
if (!hasReplaceStep) {
|
|
170
|
+
if (hasStructuralStep || !hasReplaceStep) {
|
|
161
171
|
return 'other';
|
|
162
172
|
}
|
|
163
173
|
const {
|
|
164
174
|
selection
|
|
165
175
|
} = state;
|
|
166
|
-
const isNodeSelected = selection instanceof NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
|
|
176
|
+
const isNodeSelected = selection instanceof NodeSelection && (((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock' || isSyncBlockActivationEnabled && ((_selection$node2 = selection.node) === null || _selection$node2 === void 0 ? void 0 : _selection$node2.type.name) === 'syncBlock');
|
|
167
177
|
return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
|
|
168
178
|
};
|
|
179
|
+
export const getPromptedFeedbackEntryPoint = mechanism => {
|
|
180
|
+
if (mechanism === 'undo') {
|
|
181
|
+
return 'prompted-undo';
|
|
182
|
+
}
|
|
183
|
+
if (mechanism === 'deleteButton' || mechanism === 'keyboardDelete' || mechanism === 'selectionReplaced') {
|
|
184
|
+
return 'prompted-delete';
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
};
|
|
169
188
|
/**
|
|
170
189
|
* True when a source `bodiedSyncBlock` holds user content — i.e. not just the
|
|
171
190
|
* single empty paragraph it is created with (used to detect empty→content).
|
|
@@ -211,6 +230,7 @@ const filterTransactionOnline = ({
|
|
|
211
230
|
syncBlockStore,
|
|
212
231
|
api,
|
|
213
232
|
confirmationTransactionRef,
|
|
233
|
+
ctx,
|
|
214
234
|
bodiedSyncBlockRemoved,
|
|
215
235
|
bodiedSyncBlockAdded,
|
|
216
236
|
extensionFlagShown
|
|
@@ -241,6 +261,19 @@ const filterTransactionOnline = ({
|
|
|
241
261
|
// dedicated `cross_product_paste` event subject.
|
|
242
262
|
const isPaste = Boolean((_tr$getMeta = tr.getMeta('paste')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : tr.getMeta('uiEvent') === 'paste');
|
|
243
263
|
const isPasteOrDrop = isPaste || tr.getMeta('uiEvent') === 'drop';
|
|
264
|
+
const hasRemovedSyncBlock = syncBlockRemoved.length > 0 || bodiedSyncBlockRemoved.length > 0;
|
|
265
|
+
const isCutRemoval = hasRemovedSyncBlock && ctx.consumeCutRemoval();
|
|
266
|
+
const canPromptForRemoval = Boolean(ctx.onGiveFeedback) && hasRemovedSyncBlock && !isDirtyTransaction(tr) && !isCutRemoval && !isPasteOrDrop;
|
|
267
|
+
const promptedFeedbackEntryPoint = canPromptForRemoval ? getPromptedFeedbackEntryPoint(getDeleteMechanism(tr, state)) : undefined;
|
|
268
|
+
const entryPoint = promptedFeedbackEntryPoint && expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true) ? promptedFeedbackEntryPoint : undefined;
|
|
269
|
+
if (entryPoint && syncBlockRemoved.length > 0 && bodiedSyncBlockRemoved.length === 0) {
|
|
270
|
+
tr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
|
|
271
|
+
context: {
|
|
272
|
+
blockType: 'reference',
|
|
273
|
+
entryPoint
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
244
277
|
syncBlockAdded.forEach(syncBlock => {
|
|
245
278
|
var _api$analytics2, _api$analytics2$actio;
|
|
246
279
|
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({
|
|
@@ -268,7 +301,22 @@ const filterTransactionOnline = ({
|
|
|
268
301
|
}
|
|
269
302
|
// Only derive the mechanism behind the gate — discarded gate-off.
|
|
270
303
|
const mechanism = fg('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
|
|
271
|
-
|
|
304
|
+
const feedbackContext = entryPoint && getDeleteReason(tr) !== 'source-block-unsynced' ? {
|
|
305
|
+
blockType: 'source',
|
|
306
|
+
entryPoint
|
|
307
|
+
} : undefined;
|
|
308
|
+
if (feedbackContext) {
|
|
309
|
+
const sourceAttempt = ctx.queueSourceFeedback(feedbackContext);
|
|
310
|
+
return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, {
|
|
311
|
+
onDeleteCompleted: success => ctx.completeSourceDeletion(sourceAttempt, success),
|
|
312
|
+
onDeleteTransaction: deleteTr => deleteTr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
|
|
313
|
+
context: feedbackContext,
|
|
314
|
+
sourceAttempt
|
|
315
|
+
}),
|
|
316
|
+
onDestroy: () => ctx.clearSourceFeedback(sourceAttempt)
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, undefined);
|
|
272
320
|
}
|
|
273
321
|
if (bodiedSyncBlockAdded.length > 0) {
|
|
274
322
|
if (tr.getMeta(pmHistoryPluginKey)) {
|
|
@@ -391,14 +439,17 @@ const buildStatusDecorations = (doc, syncBlockStore, isOffline, isViewMode, isDr
|
|
|
391
439
|
* is explicitly scoped to a single plugin instance.
|
|
392
440
|
*/
|
|
393
441
|
class SyncedBlockPluginContext {
|
|
394
|
-
constructor() {
|
|
442
|
+
constructor(onGiveFeedback) {
|
|
395
443
|
_defineProperty(this, "confirmationTransactionRef", {
|
|
396
444
|
current: undefined
|
|
397
445
|
});
|
|
446
|
+
_defineProperty(this, "_cutRemovalPending", false);
|
|
447
|
+
_defineProperty(this, "cutRemovalGeneration", 0);
|
|
398
448
|
_defineProperty(this, "_isCopyEvent", false);
|
|
399
449
|
_defineProperty(this, "_isCutEvent", false);
|
|
400
450
|
_defineProperty(this, "unpublishedFlagShown", new Set());
|
|
401
451
|
_defineProperty(this, "extensionFlagShown", new Set());
|
|
452
|
+
this.onGiveFeedback = onGiveFeedback;
|
|
402
453
|
}
|
|
403
454
|
get isCopyEvent() {
|
|
404
455
|
return this._isCopyEvent;
|
|
@@ -419,6 +470,86 @@ class SyncedBlockPluginContext {
|
|
|
419
470
|
this._isCutEvent = false;
|
|
420
471
|
return was;
|
|
421
472
|
}
|
|
473
|
+
markCutRemovalPending() {
|
|
474
|
+
this._cutRemovalPending = true;
|
|
475
|
+
const generation = ++this.cutRemovalGeneration;
|
|
476
|
+
queueMicrotask(() => {
|
|
477
|
+
if (this.cutRemovalGeneration === generation) {
|
|
478
|
+
this._cutRemovalPending = false;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
consumeCutRemoval() {
|
|
483
|
+
const wasPending = this._cutRemovalPending;
|
|
484
|
+
this._cutRemovalPending = false;
|
|
485
|
+
this.cutRemovalGeneration += 1;
|
|
486
|
+
return wasPending;
|
|
487
|
+
}
|
|
488
|
+
queueSourceFeedback(context) {
|
|
489
|
+
const sourceAttempt = Symbol('syncedBlockSourceFeedbackAttempt');
|
|
490
|
+
this.pendingSourceFeedback = {
|
|
491
|
+
context,
|
|
492
|
+
deletionSucceeded: false,
|
|
493
|
+
sourceAttempt,
|
|
494
|
+
transactionApplied: false
|
|
495
|
+
};
|
|
496
|
+
return sourceAttempt;
|
|
497
|
+
}
|
|
498
|
+
completeSourceDeletion(sourceAttempt, success) {
|
|
499
|
+
var _this$pendingSourceFe;
|
|
500
|
+
if (((_this$pendingSourceFe = this.pendingSourceFeedback) === null || _this$pendingSourceFe === void 0 ? void 0 : _this$pendingSourceFe.sourceAttempt) !== sourceAttempt) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (!success) {
|
|
504
|
+
// Keep the attempt pending because retryDeletion reuses these callbacks.
|
|
505
|
+
// Dismissal or cancellation clears it through the destroy callback.
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.pendingSourceFeedback.deletionSucceeded = true;
|
|
509
|
+
this.flushSourceFeedback(sourceAttempt);
|
|
510
|
+
}
|
|
511
|
+
clearSourceFeedback(sourceAttempt) {
|
|
512
|
+
var _this$pendingSourceFe2;
|
|
513
|
+
if (((_this$pendingSourceFe2 = this.pendingSourceFeedback) === null || _this$pendingSourceFe2 === void 0 ? void 0 : _this$pendingSourceFe2.sourceAttempt) === sourceAttempt) {
|
|
514
|
+
this.pendingSourceFeedback = undefined;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
handleAppliedFeedback({
|
|
518
|
+
context,
|
|
519
|
+
sourceAttempt
|
|
520
|
+
}) {
|
|
521
|
+
var _this$pendingSourceFe3;
|
|
522
|
+
if (context.blockType === 'source' && sourceAttempt && ((_this$pendingSourceFe3 = this.pendingSourceFeedback) === null || _this$pendingSourceFe3 === void 0 ? void 0 : _this$pendingSourceFe3.sourceAttempt) === sourceAttempt) {
|
|
523
|
+
this.pendingSourceFeedback.transactionApplied = true;
|
|
524
|
+
this.flushSourceFeedback(sourceAttempt);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (context.blockType === 'reference') {
|
|
528
|
+
this.invokeFeedback(context);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
flushSourceFeedback(sourceAttempt) {
|
|
532
|
+
var _this$pendingSourceFe4, _this$pendingSourceFe5;
|
|
533
|
+
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) {
|
|
534
|
+
const {
|
|
535
|
+
context
|
|
536
|
+
} = this.pendingSourceFeedback;
|
|
537
|
+
this.pendingSourceFeedback = undefined;
|
|
538
|
+
this.invokeFeedback(context);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
invokeFeedback(context) {
|
|
542
|
+
deferDispatch(() => {
|
|
543
|
+
try {
|
|
544
|
+
var _this$onGiveFeedback;
|
|
545
|
+
const feedbackResult = (_this$onGiveFeedback = this.onGiveFeedback) === null || _this$onGiveFeedback === void 0 ? void 0 : _this$onGiveFeedback.call(this, context);
|
|
546
|
+
void Promise.resolve(feedbackResult).catch(() => {});
|
|
547
|
+
} catch {
|
|
548
|
+
// Product feedback collectors are optional UI. Their failures must
|
|
549
|
+
// never affect the already-applied editor transaction.
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
422
553
|
}
|
|
423
554
|
export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api) => {
|
|
424
555
|
const {
|
|
@@ -430,7 +561,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
430
561
|
// and avoids ~10 redundant Statsig SDK evaluations per keystroke in hot paths
|
|
431
562
|
// (apply, filterTransaction, appendTransaction, decorations).
|
|
432
563
|
const isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
|
|
433
|
-
const ctx = new SyncedBlockPluginContext();
|
|
564
|
+
const ctx = new SyncedBlockPluginContext(options === null || options === void 0 ? void 0 : options.onGiveFeedback);
|
|
434
565
|
const confirmationTransactionRef = ctx.confirmationTransactionRef;
|
|
435
566
|
const unpublishedFlagShown = ctx.unpublishedFlagShown;
|
|
436
567
|
const extensionFlagShown = ctx.extensionFlagShown;
|
|
@@ -781,7 +912,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
781
912
|
return false;
|
|
782
913
|
},
|
|
783
914
|
cut: () => {
|
|
784
|
-
if (fg('platform_synced_block_patch_13')) {
|
|
915
|
+
if (fg('platform_synced_block_patch_13') || Boolean(options === null || options === void 0 ? void 0 : options.onGiveFeedback) && expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true)) {
|
|
785
916
|
ctx.consumeCutEvent();
|
|
786
917
|
ctx.markCutEvent();
|
|
787
918
|
}
|
|
@@ -822,20 +953,40 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
822
953
|
} = state;
|
|
823
954
|
const isCopy = ctx.consumeCopyEvent();
|
|
824
955
|
const isSyncedBlockPatch13Enabled = fg('platform_synced_block_patch_13');
|
|
825
|
-
const
|
|
826
|
-
|
|
956
|
+
const wasCut = ctx.consumeCutEvent();
|
|
957
|
+
const isCut = isSyncedBlockPatch13Enabled && wasCut;
|
|
958
|
+
const isPromptedFeedbackCut = Boolean(syncBlockStore && (options === null || options === void 0 ? void 0 : options.onGiveFeedback) && wasCut) && expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true);
|
|
959
|
+
if (!syncBlockStore || !isCopy && !isCut && !isPromptedFeedbackCut) {
|
|
827
960
|
return slice;
|
|
828
961
|
}
|
|
829
|
-
|
|
962
|
+
if (isPromptedFeedbackCut && !isCut) {
|
|
963
|
+
let containsCutSyncBlock = false;
|
|
964
|
+
mapSlice(slice, node => {
|
|
965
|
+
if (syncBlockStore.referenceManager.isReferenceBlock(node) || node.type.name === 'bodiedSyncBlock' && sliceFullyContainsNode(slice, node)) {
|
|
966
|
+
containsCutSyncBlock = true;
|
|
967
|
+
}
|
|
968
|
+
return node;
|
|
969
|
+
});
|
|
970
|
+
if (containsCutSyncBlock) {
|
|
971
|
+
ctx.markCutRemovalPending();
|
|
972
|
+
}
|
|
973
|
+
return slice;
|
|
974
|
+
}
|
|
975
|
+
let containsCutSyncBlock = false;
|
|
976
|
+
const transformedSlice = mapSlice(slice, node => {
|
|
830
977
|
if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
|
|
831
978
|
var _syncBlockStore$refer, _syncBlockStore$refer2;
|
|
832
979
|
if (isCut) {
|
|
980
|
+
containsCutSyncBlock = isPromptedFeedbackCut;
|
|
833
981
|
return node;
|
|
834
982
|
}
|
|
835
983
|
showCopiedFlag(api, options === null || options === void 0 ? void 0 : options.__livePage, ((_syncBlockStore$refer = syncBlockStore.referenceManager.getFromCache(node.attrs.resourceId)) === null || _syncBlockStore$refer === void 0 ? void 0 : (_syncBlockStore$refer2 = _syncBlockStore$refer.data) === null || _syncBlockStore$refer2 === void 0 ? void 0 : _syncBlockStore$refer2.status) === 'unpublished', getSourceProductFromResourceIdSafe(node.attrs.resourceId));
|
|
836
984
|
return node;
|
|
837
985
|
}
|
|
838
986
|
if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
|
|
987
|
+
if (isPromptedFeedbackCut && isCut && sliceFullyContainsNode(slice, node)) {
|
|
988
|
+
containsCutSyncBlock = true;
|
|
989
|
+
}
|
|
839
990
|
// if we only selected part of the bodied sync block content,
|
|
840
991
|
// remove the sync block node and only keep the content
|
|
841
992
|
if (!sliceFullyContainsNode(slice, node)) {
|
|
@@ -860,6 +1011,10 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
860
1011
|
}
|
|
861
1012
|
return node;
|
|
862
1013
|
});
|
|
1014
|
+
if (containsCutSyncBlock) {
|
|
1015
|
+
ctx.markCutRemovalPending();
|
|
1016
|
+
}
|
|
1017
|
+
return transformedSlice;
|
|
863
1018
|
}
|
|
864
1019
|
},
|
|
865
1020
|
filterTransaction: (tr, state) => {
|
|
@@ -939,6 +1094,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
939
1094
|
syncBlockStore,
|
|
940
1095
|
api,
|
|
941
1096
|
confirmationTransactionRef,
|
|
1097
|
+
ctx,
|
|
942
1098
|
bodiedSyncBlockRemoved,
|
|
943
1099
|
bodiedSyncBlockAdded,
|
|
944
1100
|
extensionFlagShown
|
|
@@ -962,6 +1118,17 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
962
1118
|
if (viewMode === 'view') {
|
|
963
1119
|
return null;
|
|
964
1120
|
}
|
|
1121
|
+
if (options !== null && options !== void 0 && options.onGiveFeedback) {
|
|
1122
|
+
const feedbackContexts = trs.flatMap(tr => {
|
|
1123
|
+
const feedbackContext = tr.getMeta(syncedBlockPromptedFeedbackMetaKey);
|
|
1124
|
+
return feedbackContext ? [feedbackContext] : [];
|
|
1125
|
+
});
|
|
1126
|
+
if (feedbackContexts.length > 0 && expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true)) {
|
|
1127
|
+
feedbackContexts.forEach(feedbackContext => {
|
|
1128
|
+
ctx.handleAppliedFeedback(feedbackContext);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
965
1132
|
|
|
966
1133
|
// Update source sync block cache for user-initiated changes only.
|
|
967
1134
|
// Cache updates are handled here instead of in the nodeview update() so we
|
|
@@ -2,7 +2,7 @@ import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
|
|
|
2
2
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
3
3
|
import { FLAG_ID } from '../../types';
|
|
4
4
|
import { syncedBlockPluginKey } from '../main';
|
|
5
|
-
import { recomputeDeleteTransaction } from './recompute-delete-transaction';
|
|
5
|
+
import { recomputeDeleteTransaction, recomputeUnsyncTransaction } from './recompute-delete-transaction';
|
|
6
6
|
const onRetry = (api, syncBlockStore) => () => {
|
|
7
7
|
var _api$core;
|
|
8
8
|
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
|
|
@@ -21,7 +21,7 @@ const onDismissed = syncBlockStore => tr => {
|
|
|
21
21
|
bodiedSyncBlockDeletionStatus: 'none'
|
|
22
22
|
});
|
|
23
23
|
};
|
|
24
|
-
export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) => {
|
|
24
|
+
export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism, feedbackCallbacks) => {
|
|
25
25
|
// Clear potential old pending deletion to retreat the deletion as first attempt
|
|
26
26
|
syncBlockStore.sourceManager.clearPendingDeletion();
|
|
27
27
|
|
|
@@ -42,7 +42,13 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
|
|
|
42
42
|
api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
|
|
43
43
|
tr
|
|
44
44
|
}) => {
|
|
45
|
-
|
|
45
|
+
// EDITOR-8230: unsync must remove only the sync wrapper and keep the block's
|
|
46
|
+
// content inline, whereas delete removes the whole block. Before this branch,
|
|
47
|
+
// the confirm path always recomputed a full delete, so unsyncing a source block
|
|
48
|
+
// silently deleted its content. When the deletion was actually triggered by
|
|
49
|
+
// unsync, recompute an unwrap (replaceWith content) instead of a delete.
|
|
50
|
+
const isUnsync = fg('platform_editor_blocks_patch_6') && deletionReason === 'source-block-unsynced';
|
|
51
|
+
const recomputedTr = isUnsync ? recomputeUnsyncTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs)) : recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs));
|
|
46
52
|
|
|
47
53
|
// The target node(s) no longer exist in the live document (e.g.
|
|
48
54
|
// a remote collaborator already removed them). There is nothing
|
|
@@ -54,6 +60,7 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
|
|
|
54
60
|
return null;
|
|
55
61
|
}
|
|
56
62
|
recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
|
|
63
|
+
feedbackCallbacks === null || feedbackCallbacks === void 0 ? void 0 : feedbackCallbacks.onDeleteTransaction(recomputedTr);
|
|
57
64
|
if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
|
|
58
65
|
// bodiedSyncBlock deletion is expected to be permanent (cannot undo)
|
|
59
66
|
// For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
|
|
@@ -69,6 +76,7 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
|
|
|
69
76
|
}
|
|
70
77
|
api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(() => {
|
|
71
78
|
const trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
|
|
79
|
+
feedbackCallbacks === null || feedbackCallbacks === void 0 ? void 0 : feedbackCallbacks.onDeleteTransaction(trToDispatch);
|
|
72
80
|
if (!trToDispatch.getMeta(pmHistoryPluginKey)) {
|
|
73
81
|
// bodiedSyncBlock deletion is expected to be permanent (cannot undo)
|
|
74
82
|
// For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
|
|
@@ -78,6 +86,7 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
|
|
|
78
86
|
});
|
|
79
87
|
}, success => {
|
|
80
88
|
var _api$core4;
|
|
89
|
+
feedbackCallbacks === null || feedbackCallbacks === void 0 ? void 0 : feedbackCallbacks.onDeleteCompleted(success);
|
|
81
90
|
api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions.execute(({
|
|
82
91
|
tr
|
|
83
92
|
}) => {
|
|
@@ -107,6 +116,7 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
|
|
|
107
116
|
});
|
|
108
117
|
}, () => {
|
|
109
118
|
confirmationTransactionRef.current = undefined;
|
|
119
|
+
feedbackCallbacks === null || feedbackCallbacks === void 0 ? void 0 : feedbackCallbacks.onDestroy();
|
|
110
120
|
}, mechanism);
|
|
111
121
|
return false;
|
|
112
122
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Slice } from '@atlaskit/editor-prosemirror/model';
|
|
1
2
|
/**
|
|
2
3
|
* Whether `node` is the source block requested by `wanted`.
|
|
3
4
|
*
|
|
@@ -87,4 +88,55 @@ export const recomputeDeleteTransaction = (tr, isSourceBlock, syncBlockIds) => {
|
|
|
87
88
|
tr.delete(pos, pos + node.nodeSize);
|
|
88
89
|
});
|
|
89
90
|
return tr;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Recompute a source `bodiedSyncBlock` *unsync* from the live document.
|
|
95
|
+
*
|
|
96
|
+
* Unsync differs from delete: the sync wrapper must be removed while its content
|
|
97
|
+
* is preserved inline in the document. Instead of `tr.delete(pos, pos +
|
|
98
|
+
* nodeSize)` (which drops the content too — EDITOR-8230), this replaces each
|
|
99
|
+
* matched source block with its own content.
|
|
100
|
+
*
|
|
101
|
+
* The replacement uses a raw `tr.replace(pos, pos + nodeSize, new Slice(content,
|
|
102
|
+
* 0, 0))` — an explicit zero-open slice inserted verbatim — rather than
|
|
103
|
+
* `tr.replaceWith(...)`. `replaceWith` routes through ProseMirror's
|
|
104
|
+
* `replaceRange`, whose range-fitting heuristics can collapse or drop a trailing
|
|
105
|
+
* block (e.g. the block's final panel/empty paragraph) when the unwrapped
|
|
106
|
+
* content meets a document boundary, silently losing content (EDITOR-8230). A
|
|
107
|
+
* zero-open `replace` drops the wrapper's own open ends while keeping every child
|
|
108
|
+
* of the fragment as its own top-level node.
|
|
109
|
+
*
|
|
110
|
+
* Like `recomputeDeleteTransaction`, targets are located in the *current*
|
|
111
|
+
* document (`tr.doc`) so the operation is robust to intervening local edits and
|
|
112
|
+
* remote collab changes while the confirmation modal is open, and it handles
|
|
113
|
+
* partial matches gracefully (missing blocks are skipped).
|
|
114
|
+
*
|
|
115
|
+
* Replacements are applied in reverse document order so earlier replacements do
|
|
116
|
+
* not invalidate the positions of later ones.
|
|
117
|
+
*
|
|
118
|
+
* @returns the mutated transaction when at least one target node was found and
|
|
119
|
+
* unwrapped, otherwise `undefined` when none of the targets exist any more.
|
|
120
|
+
*/
|
|
121
|
+
export const recomputeUnsyncTransaction = (tr, isSourceBlock, syncBlockIds) => {
|
|
122
|
+
const matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
|
|
123
|
+
if (matches.length === 0) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Unwrap in reverse document order so positions remain valid across edits.
|
|
128
|
+
// Use a raw zero-open Slice replace (not `replaceWith`) so the block's content
|
|
129
|
+
// is inserted verbatim as top-level nodes without ProseMirror's replaceRange
|
|
130
|
+
// heuristics collapsing a trailing block (EDITOR-8230).
|
|
131
|
+
const orderedMatches = matches.sort((a, b) => b.pos - a.pos);
|
|
132
|
+
for (const {
|
|
133
|
+
node,
|
|
134
|
+
pos
|
|
135
|
+
} of orderedMatches) {
|
|
136
|
+
// This is ProseMirror's Transaction.replace (a document edit), not String.replace — the
|
|
137
|
+
// perf rule misfires on the method name, and there is nothing to hoist.
|
|
138
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
139
|
+
tr.replace(pos, pos + node.nodeSize, new Slice(node.content, 0, 0));
|
|
140
|
+
}
|
|
141
|
+
return tr;
|
|
90
142
|
};
|
|
@@ -26,6 +26,7 @@ export const getToolbarConfig = (state, intl, api, syncBlockStore, isLivePage, o
|
|
|
26
26
|
if (syncBlockStore.sourceManager.isPendingCreation(syncBlockObject.node.attrs.resourceId)) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
+
const isSyncBlockActivationEnabled = expValEquals('platform_editor_sync_block_activation', 'isEnabled', true);
|
|
29
30
|
const syncBlockInstance = syncBlockStore.referenceManager.getFromCache(syncBlockObject.node.attrs.resourceId);
|
|
30
31
|
const isUnsyncedBlock = (syncBlockInstance === null || syncBlockInstance === void 0 ? void 0 : (_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.type) === SyncBlockError.NotFound;
|
|
31
32
|
const isErroredBlock = syncBlockInstance === null || syncBlockInstance === void 0 ? void 0 : syncBlockInstance.error;
|
|
@@ -53,7 +54,6 @@ export const getToolbarConfig = (state, intl, api, syncBlockStore, isLivePage, o
|
|
|
53
54
|
onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false, className)
|
|
54
55
|
});
|
|
55
56
|
const items = [];
|
|
56
|
-
const isSyncBlockActivationEnabled = expValEquals('platform_editor_sync_block_activation', 'isEnabled', true);
|
|
57
57
|
if (isUnsyncedBlock) {
|
|
58
58
|
const deleteButton = {
|
|
59
59
|
type: 'button',
|
|
@@ -192,7 +192,8 @@ export const getToolbarConfig = (state, intl, api, syncBlockStore, isLivePage, o
|
|
|
192
192
|
tr
|
|
193
193
|
}) => tr.setSelection(new GapCursorSelection(tr.doc.resolve(syncBlockObject.pos + syncBlockObject.node.nodeSize), Side.RIGHT)));
|
|
194
194
|
onGiveFeedback({
|
|
195
|
-
blockType: isBodiedSyncBlock ? 'source' : 'reference'
|
|
195
|
+
blockType: isBodiedSyncBlock ? 'source' : 'reference',
|
|
196
|
+
entryPoint: 'overflow-menu'
|
|
196
197
|
});
|
|
197
198
|
return true;
|
|
198
199
|
}
|
|
@@ -9,6 +9,7 @@ import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosem
|
|
|
9
9
|
import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
|
|
10
10
|
import { getSourceProductFromResourceIdSafe } from '@atlaskit/editor-synced-block-provider/utils';
|
|
11
11
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
12
|
+
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
12
13
|
import { creationMetaKey, deleteMechanismMetaKey, syncedBlockPluginKey } from '../pm-plugins/main';
|
|
13
14
|
import { canBeConvertedToSyncBlock, deferDispatch, findSyncBlock, findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
|
|
14
15
|
import { FLAG_ID } from '../types';
|
|
@@ -348,7 +349,11 @@ export var removeSyncedBlockAtPos = function removeSyncedBlockAtPos(api, pos) {
|
|
|
348
349
|
var node = tr.doc.nodeAt(pos);
|
|
349
350
|
if ((node === null || node === void 0 ? void 0 : node.type.name) === 'syncBlock') {
|
|
350
351
|
var _node$nodeSize;
|
|
351
|
-
|
|
352
|
+
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));
|
|
353
|
+
if (expValEqualsNoExposure('platform_editor_sync_block_activation', 'isEnabled', true)) {
|
|
354
|
+
removeTr.setMeta(deleteMechanismMetaKey, 'deleteButton');
|
|
355
|
+
}
|
|
356
|
+
return removeTr;
|
|
352
357
|
}
|
|
353
358
|
return tr;
|
|
354
359
|
});
|
|
@@ -367,6 +372,12 @@ export var unsync = function unsync(storeManager, isBodiedSyncBlock, view) {
|
|
|
367
372
|
return false;
|
|
368
373
|
}
|
|
369
374
|
if (isBodiedSyncBlock) {
|
|
375
|
+
// Signal the unsync intent. This transaction is intercepted by the plugin's
|
|
376
|
+
// filterTransaction, which shows the deletion-confirmation modal and, on confirm,
|
|
377
|
+
// recomputes the actual document change from the live document keyed off the
|
|
378
|
+
// `deletionReason: 'source-block-unsynced'` meta (see handleBodiedSyncBlockRemoval /
|
|
379
|
+
// recomputeUnsyncTransaction). The real unwrap-vs-delete decision therefore lives in the
|
|
380
|
+
// confirm path, not here (EDITOR-8230).
|
|
370
381
|
var content = syncBlock === null || syncBlock === void 0 ? void 0 : syncBlock.node.content;
|
|
371
382
|
var tr = state.tr;
|
|
372
383
|
tr.replaceWith(syncBlock.pos, syncBlock.pos + syncBlock.node.nodeSize, content).setMeta('deletionReason', 'source-block-unsynced');
|