@atlaskit/editor-plugin-synced-block 10.4.6 → 10.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **For workflow guidance, debugging, and cross-package task guides, load the `synced-blocks`
4
4
  > skill:**
5
- > `get_skill(skill_name_or_path="platform/packages/editor/.rovodev/skills/synced-blocks/SKILL.md")`
5
+ > `get_skill(skill_name_or_path="platform/packages/editor/.agents/skills/synced-blocks/SKILL.md")`
6
6
 
7
7
  ---
8
8
 
@@ -31,7 +31,8 @@ src/
31
31
  │ discardUnpublishedSyncBlocks (EDITOR-6473)
32
32
  ├── editor-commands/
33
33
  │ └── index.ts # createSyncedBlock, copySyncedBlockReferenceToClipboardEditorCommand,
34
- │ removeSyncedBlockAtPos, unsyncSyncBlock
34
+ │ copySyncedBlockReferenceToClipboard, editSyncedBlockSource,
35
+ │ removeSyncedBlock, removeSyncedBlockAtPos, unsync
35
36
  ├── nodeviews/
36
37
  │ ├── syncedBlock.tsx # NodeView for reference (syncBlock) — read-only, fetches from BE
37
38
  │ ├── lazySyncedBlock.tsx # Lazy-loaded wrapper for syncedBlock (EDITOR-6928)
@@ -50,13 +51,18 @@ src/
50
51
  │ ├── rebase-transaction.ts # Rebase helpers used by main.ts
51
52
  │ ├── ignore-dom-event.ts # DOM event guard
52
53
  │ └── utils.ts # Misc shared helpers
53
- ├── ui/
54
+ ├── ui/ # (grep the dir for the full current list — it grows often)
54
55
  │ ├── toolbar-components.tsx # Primary toolbar button ("Create Synced Block")
56
+ │ ├── CreateSyncedBlockButton.tsx / CreateSyncedBlockDropdownItem.tsx # Toolbar/menu entry points
55
57
  │ ├── floating-toolbar.tsx # Node-level actions: delete, unsync, copy link, view locations
56
58
  │ ├── block-menu-components.tsx # Block menu entry
57
59
  │ ├── quick-insert.tsx # Slash command / quick insert config
60
+ │ ├── SyncedLocationDropdown.tsx # "View synced locations" dropdown
58
61
  │ ├── DeleteConfirmationModal.tsx # Deletion confirmation dialog
59
62
  │ ├── SyncBlockRefresher.tsx # Periodic data refresh from backend
63
+ │ ├── SyncBlockLabel.tsx # Source/reference label chrome
64
+ │ ├── BodiedSyncBlockWrapper.tsx / SyncBlockRendererWrapper.tsx # Node-view wrappers
65
+ │ ├── SyncBlockSSRReactContextsProvider.tsx # Supplies React contexts during SSR
60
66
  │ └── Flag.tsx # Error/info flags (offline, copy notifications)
61
67
  └── types/
62
68
  └── index.ts # FLAG_ID, SyncedBlockSharedState, BodiedSyncBlockDeletionStatus
@@ -89,14 +95,18 @@ Behind the `editor_synced_block_perf` experiment, `main.ts`:
89
95
 
90
96
  1. User triggers via toolbar/block menu/slash command → `ui/toolbar-components.tsx` or
91
97
  `ui/block-menu-components.tsx`
92
- 2. Calls `editor-commands/createSyncedBlock` → inserts `bodiedSyncBlock` node into document
93
- 3. `pm-plugins/main.ts` detects new node → `handle-bodied-sync-block-creation.ts` → calls
94
- `SyncBlockStoreManager.create()` → Block Service API
95
- 4. `menu-and-toolbar-experiences.ts` fires experience event
98
+ 2. Calls `editor-commands/createSyncedBlock` → inserts `bodiedSyncBlock` node into document (marked
99
+ as **pending creation** — it is not persisted yet)
100
+ 3. `pm-plugins/main.ts` detects new node → `handle-bodied-sync-block-creation.ts` updates
101
+ `sourceManager` state. Persistence happens later when the product layer calls `flush()`
102
+ (via `flushBodiedSyncBlocks`), which creates the block in the Block Service and then reconciles
103
+ identifiers via `commitPendingCreation()`
104
+ 4. `menu-and-toolbar-experiences.ts` fires the experience event
96
105
 
97
106
  **Reference rendering** (flow through the code):
98
107
 
99
- 1. `nodeviews/syncedBlock.tsx` mounts for each `syncBlock` node
100
- 2. Calls `ReferenceSyncBlockStoreManager.fetch(resourceId)` → Block Service API
108
+ 1. `nodeviews/syncedBlock.tsx` (or `lazySyncedBlock.tsx`) mounts for each `syncBlock` node
109
+ 2. Calls `referenceManager.fetchSyncBlocksData(nodes)` → batched/deduped Block Service fetch
101
110
  3. Renders content via nested renderer from `editor-synced-block-renderer`
102
- 4. Subscribes to AGG WebSocket for real-time updates
111
+ 4. Subscribes via `referenceManager.subscribeToSyncBlock(...)` — AGG WebSocket (Confluence) or Relay
112
+ (Jira) — for real-time updates
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 10.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`3d4d7717366f7`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/3d4d7717366f7) -
8
+ Prompt for feedback after eligible synced block deletion or undo
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies
13
+
14
+ ## 10.4.7
15
+
16
+ ### Patch Changes
17
+
18
+ - [`d9e8a63a60b63`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/d9e8a63a60b63) -
19
+ Keep the placeholder visible in empty source synced blocks for the sync block activation treatment
20
+ while preserving the existing placeholder behavior in the control.
21
+
3
22
  ## 10.4.6
4
23
 
5
24
  ### Patch Changes
@@ -14,6 +14,7 @@ var _state = require("@atlaskit/editor-prosemirror/state");
14
14
  var _utils = require("@atlaskit/editor-prosemirror/utils");
15
15
  var _utils2 = require("@atlaskit/editor-synced-block-provider/utils");
16
16
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
17
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
17
18
  var _main = require("../pm-plugins/main");
18
19
  var _utils3 = require("../pm-plugins/utils/utils");
19
20
  var _types = require("../types");
@@ -355,7 +356,11 @@ var removeSyncedBlockAtPos = exports.removeSyncedBlockAtPos = function removeSyn
355
356
  var node = tr.doc.nodeAt(pos);
356
357
  if ((node === null || node === void 0 ? void 0 : node.type.name) === 'syncBlock') {
357
358
  var _node$nodeSize;
358
- return tr.replace(pos, pos + ((_node$nodeSize = node === null || node === void 0 ? void 0 : node.nodeSize) !== null && _node$nodeSize !== void 0 ? _node$nodeSize : 0));
359
+ var removeTr = tr.replace(pos, pos + ((_node$nodeSize = node === null || node === void 0 ? void 0 : node.nodeSize) !== null && _node$nodeSize !== void 0 ? _node$nodeSize : 0));
360
+ if ((0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true)) {
361
+ removeTr.setMeta(_main.deleteMechanismMetaKey, 'deleteButton');
362
+ }
363
+ return removeTr;
359
364
  }
360
365
  return tr;
361
366
  });
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.syncedBlockPluginKey = exports.getDeleteMechanism = exports.deleteMechanismMetaKey = exports.creationMetaKey = exports.createPlugin = void 0;
7
+ exports.syncedBlockPluginKey = exports.getPromptedFeedbackEntryPoint = exports.getDeleteMechanism = exports.deleteMechanismMetaKey = exports.creationMetaKey = exports.createPlugin = void 0;
8
8
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
@@ -24,6 +24,7 @@ var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider
24
24
  var _utils2 = require("@atlaskit/editor-synced-block-provider/utils");
25
25
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
26
26
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
27
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
27
28
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
28
29
  var _bodiedSyncedBlock = require("../nodeviews/bodiedSyncedBlock");
29
30
  var _syncedBlock = require("../nodeviews/syncedBlock");
@@ -53,6 +54,7 @@ var syncedBlockPluginKey = exports.syncedBlockPluginKey = new _state.PluginKey('
53
54
  * `platform_editor_blocks_patch_4`.
54
55
  */
55
56
  var deleteMechanismMetaKey = exports.deleteMechanismMetaKey = new _state.PluginKey('syncedBlockDeleteMechanism');
57
+ var syncedBlockPromptedFeedbackMetaKey = new _state.PluginKey('syncedBlockPromptedFeedback');
56
58
 
57
59
  /**
58
60
  * Creation-type signals set by {@link createSyncedBlock} on the creating
@@ -149,7 +151,7 @@ var getDeleteReason = function getDeleteReason(tr) {
149
151
 
150
152
  /** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
151
153
  var isHistoryMeta = function isHistoryMeta(meta) {
152
- return (0, _typeof2.default)(meta) === 'object' && meta !== null && 'redo' in meta;
154
+ return (0, _typeof2.default)(meta) === 'object' && meta !== null && 'redo' in meta && typeof meta.redo === 'boolean';
153
155
  };
154
156
 
155
157
  /**
@@ -174,9 +176,16 @@ var isHistoryMeta = function isHistoryMeta(meta) {
174
176
  * selected when the edit was made.
175
177
  */
176
178
  var getDeleteMechanism = exports.getDeleteMechanism = function getDeleteMechanism(tr, state) {
177
- var _selection$node;
179
+ var _selection$node, _selection$node2;
180
+ // Keep the legacy analytics classification unchanged while activation is off.
181
+ // The stricter history, structural-step, and reference-selection handling is
182
+ // only needed by the prompted feedback rollout.
183
+ var isSyncBlockActivationEnabled = (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true);
178
184
  var historyMeta = tr.getMeta(_utils.pmHistoryPluginKey);
179
185
  if (historyMeta) {
186
+ if (isSyncBlockActivationEnabled && !isHistoryMeta(historyMeta)) {
187
+ return 'other';
188
+ }
180
189
  return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
181
190
  }
182
191
 
@@ -185,16 +194,28 @@ var getDeleteMechanism = exports.getDeleteMechanism = function getDeleteMechanis
185
194
  if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
186
195
  return 'deleteButton';
187
196
  }
197
+ var hasStructuralStep = isSyncBlockActivationEnabled && tr.steps.some(function (step) {
198
+ return step instanceof _transform.ReplaceAroundStep;
199
+ });
188
200
  var hasReplaceStep = tr.steps.some(function (step) {
189
201
  return step instanceof _transform.ReplaceStep;
190
202
  });
191
- if (!hasReplaceStep) {
203
+ if (hasStructuralStep || !hasReplaceStep) {
192
204
  return 'other';
193
205
  }
194
206
  var selection = state.selection;
195
- var isNodeSelected = selection instanceof _state.NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
207
+ var isNodeSelected = selection instanceof _state.NodeSelection && (((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock' || isSyncBlockActivationEnabled && ((_selection$node2 = selection.node) === null || _selection$node2 === void 0 ? void 0 : _selection$node2.type.name) === 'syncBlock');
196
208
  return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
197
209
  };
210
+ var getPromptedFeedbackEntryPoint = exports.getPromptedFeedbackEntryPoint = function getPromptedFeedbackEntryPoint(mechanism) {
211
+ if (mechanism === 'undo') {
212
+ return 'prompted-undo';
213
+ }
214
+ if (mechanism === 'deleteButton' || mechanism === 'keyboardDelete' || mechanism === 'selectionReplaced') {
215
+ return 'prompted-delete';
216
+ }
217
+ return undefined;
218
+ };
198
219
  /**
199
220
  * True when a source `bodiedSyncBlock` holds user content — i.e. not just the
200
221
  * single empty paragraph it is created with (used to detect empty→content).
@@ -239,6 +260,7 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
239
260
  syncBlockStore = _ref3.syncBlockStore,
240
261
  api = _ref3.api,
241
262
  confirmationTransactionRef = _ref3.confirmationTransactionRef,
263
+ ctx = _ref3.ctx,
242
264
  bodiedSyncBlockRemoved = _ref3.bodiedSyncBlockRemoved,
243
265
  bodiedSyncBlockAdded = _ref3.bodiedSyncBlockAdded,
244
266
  extensionFlagShown = _ref3.extensionFlagShown;
@@ -266,6 +288,19 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
266
288
  // dedicated `cross_product_paste` event subject.
267
289
  var isPaste = Boolean((_tr$getMeta = tr.getMeta('paste')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : tr.getMeta('uiEvent') === 'paste');
268
290
  var isPasteOrDrop = isPaste || tr.getMeta('uiEvent') === 'drop';
291
+ var hasRemovedSyncBlock = syncBlockRemoved.length > 0 || bodiedSyncBlockRemoved.length > 0;
292
+ var isCutRemoval = hasRemovedSyncBlock && ctx.consumeCutRemoval();
293
+ var canPromptForRemoval = Boolean(ctx.onGiveFeedback) && hasRemovedSyncBlock && !(0, _collab.isDirtyTransaction)(tr) && !isCutRemoval && !isPasteOrDrop;
294
+ var promptedFeedbackEntryPoint = canPromptForRemoval ? getPromptedFeedbackEntryPoint(getDeleteMechanism(tr, state)) : undefined;
295
+ var entryPoint = promptedFeedbackEntryPoint && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true) ? promptedFeedbackEntryPoint : undefined;
296
+ if (entryPoint && syncBlockRemoved.length > 0 && bodiedSyncBlockRemoved.length === 0) {
297
+ tr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
298
+ context: {
299
+ blockType: 'reference',
300
+ entryPoint: entryPoint
301
+ }
302
+ });
303
+ }
269
304
  syncBlockAdded.forEach(function (syncBlock) {
270
305
  var _api$analytics2;
271
306
  api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || (_api$analytics2 = _api$analytics2.actions) === null || _api$analytics2 === void 0 || _api$analytics2.fireAnalyticsEvent({
@@ -293,7 +328,28 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
293
328
  }
294
329
  // Only derive the mechanism behind the gate — discarded gate-off.
295
330
  var mechanism = (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
296
- return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
331
+ var feedbackContext = entryPoint && getDeleteReason(tr) !== 'source-block-unsynced' ? {
332
+ blockType: 'source',
333
+ entryPoint: entryPoint
334
+ } : undefined;
335
+ if (feedbackContext) {
336
+ var sourceAttempt = ctx.queueSourceFeedback(feedbackContext);
337
+ return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, {
338
+ onDeleteCompleted: function onDeleteCompleted(success) {
339
+ return ctx.completeSourceDeletion(sourceAttempt, success);
340
+ },
341
+ onDeleteTransaction: function onDeleteTransaction(deleteTr) {
342
+ return deleteTr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
343
+ context: feedbackContext,
344
+ sourceAttempt: sourceAttempt
345
+ });
346
+ },
347
+ onDestroy: function onDestroy() {
348
+ return ctx.clearSourceFeedback(sourceAttempt);
349
+ }
350
+ });
351
+ }
352
+ return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, undefined);
297
353
  }
298
354
  if (bodiedSyncBlockAdded.length > 0) {
299
355
  if (tr.getMeta(_utils.pmHistoryPluginKey)) {
@@ -415,15 +471,18 @@ var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore
415
471
  * is explicitly scoped to a single plugin instance.
416
472
  */
417
473
  var SyncedBlockPluginContext = /*#__PURE__*/function () {
418
- function SyncedBlockPluginContext() {
474
+ function SyncedBlockPluginContext(onGiveFeedback) {
419
475
  (0, _classCallCheck2.default)(this, SyncedBlockPluginContext);
420
476
  (0, _defineProperty2.default)(this, "confirmationTransactionRef", {
421
477
  current: undefined
422
478
  });
479
+ (0, _defineProperty2.default)(this, "_cutRemovalPending", false);
480
+ (0, _defineProperty2.default)(this, "cutRemovalGeneration", 0);
423
481
  (0, _defineProperty2.default)(this, "_isCopyEvent", false);
424
482
  (0, _defineProperty2.default)(this, "_isCutEvent", false);
425
483
  (0, _defineProperty2.default)(this, "unpublishedFlagShown", new Set());
426
484
  (0, _defineProperty2.default)(this, "extensionFlagShown", new Set());
485
+ this.onGiveFeedback = onGiveFeedback;
427
486
  }
428
487
  return (0, _createClass2.default)(SyncedBlockPluginContext, [{
429
488
  key: "isCopyEvent",
@@ -454,19 +513,114 @@ var SyncedBlockPluginContext = /*#__PURE__*/function () {
454
513
  this._isCutEvent = false;
455
514
  return was;
456
515
  }
516
+ }, {
517
+ key: "markCutRemovalPending",
518
+ value: function markCutRemovalPending() {
519
+ var _this = this;
520
+ this._cutRemovalPending = true;
521
+ var generation = ++this.cutRemovalGeneration;
522
+ queueMicrotask(function () {
523
+ if (_this.cutRemovalGeneration === generation) {
524
+ _this._cutRemovalPending = false;
525
+ }
526
+ });
527
+ }
528
+ }, {
529
+ key: "consumeCutRemoval",
530
+ value: function consumeCutRemoval() {
531
+ var wasPending = this._cutRemovalPending;
532
+ this._cutRemovalPending = false;
533
+ this.cutRemovalGeneration += 1;
534
+ return wasPending;
535
+ }
536
+ }, {
537
+ key: "queueSourceFeedback",
538
+ value: function queueSourceFeedback(context) {
539
+ var sourceAttempt = Symbol('syncedBlockSourceFeedbackAttempt');
540
+ this.pendingSourceFeedback = {
541
+ context: context,
542
+ deletionSucceeded: false,
543
+ sourceAttempt: sourceAttempt,
544
+ transactionApplied: false
545
+ };
546
+ return sourceAttempt;
547
+ }
548
+ }, {
549
+ key: "completeSourceDeletion",
550
+ value: function completeSourceDeletion(sourceAttempt, success) {
551
+ var _this$pendingSourceFe;
552
+ if (((_this$pendingSourceFe = this.pendingSourceFeedback) === null || _this$pendingSourceFe === void 0 ? void 0 : _this$pendingSourceFe.sourceAttempt) !== sourceAttempt) {
553
+ return;
554
+ }
555
+ if (!success) {
556
+ // Keep the attempt pending because retryDeletion reuses these callbacks.
557
+ // Dismissal or cancellation clears it through the destroy callback.
558
+ return;
559
+ }
560
+ this.pendingSourceFeedback.deletionSucceeded = true;
561
+ this.flushSourceFeedback(sourceAttempt);
562
+ }
563
+ }, {
564
+ key: "clearSourceFeedback",
565
+ value: function clearSourceFeedback(sourceAttempt) {
566
+ var _this$pendingSourceFe2;
567
+ if (((_this$pendingSourceFe2 = this.pendingSourceFeedback) === null || _this$pendingSourceFe2 === void 0 ? void 0 : _this$pendingSourceFe2.sourceAttempt) === sourceAttempt) {
568
+ this.pendingSourceFeedback = undefined;
569
+ }
570
+ }
571
+ }, {
572
+ key: "handleAppliedFeedback",
573
+ value: function handleAppliedFeedback(_ref6) {
574
+ var _this$pendingSourceFe3;
575
+ var context = _ref6.context,
576
+ sourceAttempt = _ref6.sourceAttempt;
577
+ if (context.blockType === 'source' && sourceAttempt && ((_this$pendingSourceFe3 = this.pendingSourceFeedback) === null || _this$pendingSourceFe3 === void 0 ? void 0 : _this$pendingSourceFe3.sourceAttempt) === sourceAttempt) {
578
+ this.pendingSourceFeedback.transactionApplied = true;
579
+ this.flushSourceFeedback(sourceAttempt);
580
+ return;
581
+ }
582
+ if (context.blockType === 'reference') {
583
+ this.invokeFeedback(context);
584
+ }
585
+ }
586
+ }, {
587
+ key: "flushSourceFeedback",
588
+ value: function flushSourceFeedback(sourceAttempt) {
589
+ var _this$pendingSourceFe4, _this$pendingSourceFe5;
590
+ if (((_this$pendingSourceFe4 = this.pendingSourceFeedback) === null || _this$pendingSourceFe4 === void 0 ? void 0 : _this$pendingSourceFe4.sourceAttempt) === sourceAttempt && (_this$pendingSourceFe5 = this.pendingSourceFeedback) !== null && _this$pendingSourceFe5 !== void 0 && _this$pendingSourceFe5.deletionSucceeded && this.pendingSourceFeedback.transactionApplied) {
591
+ var context = this.pendingSourceFeedback.context;
592
+ this.pendingSourceFeedback = undefined;
593
+ this.invokeFeedback(context);
594
+ }
595
+ }
596
+ }, {
597
+ key: "invokeFeedback",
598
+ value: function invokeFeedback(context) {
599
+ var _this2 = this;
600
+ (0, _utils3.deferDispatch)(function () {
601
+ try {
602
+ var _this2$onGiveFeedback;
603
+ var feedbackResult = (_this2$onGiveFeedback = _this2.onGiveFeedback) === null || _this2$onGiveFeedback === void 0 ? void 0 : _this2$onGiveFeedback.call(_this2, context);
604
+ void Promise.resolve(feedbackResult).catch(function () {});
605
+ } catch (_unused) {
606
+ // Product feedback collectors are optional UI. Their failures must
607
+ // never affect the already-applied editor transaction.
608
+ }
609
+ });
610
+ }
457
611
  }]);
458
612
  }();
459
613
  var createPlugin = exports.createPlugin = function createPlugin(options, pmPluginFactoryParams, syncBlockStore, api) {
460
- var _ref6 = options || {},
461
- _ref6$useLongPressSel = _ref6.useLongPressSelection,
462
- useLongPressSelection = _ref6$useLongPressSel === void 0 ? false : _ref6$useLongPressSel;
614
+ var _ref7 = options || {},
615
+ _ref7$useLongPressSel = _ref7.useLongPressSelection,
616
+ useLongPressSelection = _ref7$useLongPressSel === void 0 ? false : _ref7$useLongPressSel;
463
617
 
464
618
  // Cache the experiment value once at plugin creation time.
465
619
  // This fires the exposure event exactly once (correct per Exposure Events 101)
466
620
  // and avoids ~10 redundant Statsig SDK evaluations per keystroke in hot paths
467
621
  // (apply, filterTransaction, appendTransaction, decorations).
468
622
  var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
469
- var ctx = new SyncedBlockPluginContext();
623
+ var ctx = new SyncedBlockPluginContext(options === null || options === void 0 ? void 0 : options.onGiveFeedback);
470
624
  var confirmationTransactionRef = ctx.confirmationTransactionRef;
471
625
  var unpublishedFlagShown = ctx.unpublishedFlagShown;
472
626
  var extensionFlagShown = ctx.extensionFlagShown;
@@ -476,8 +630,8 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
476
630
  // Classic pages with sync blocks.
477
631
  syncBlockStore.sourceManager.registerFlushCompletionCallback(function () {
478
632
  (0, _utils3.deferDispatch)(function () {
479
- api === null || api === void 0 || api.core.actions.execute(function (_ref7) {
480
- var tr = _ref7.tr;
633
+ api === null || api === void 0 || api.core.actions.execute(function (_ref8) {
634
+ var tr = _ref8.tr;
481
635
  return tr;
482
636
  });
483
637
  });
@@ -505,8 +659,8 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
505
659
  // case the Confluence default is used.
506
660
  var sourceProduct = (0, _utils2.getSourceProductFromResourceIdSafe)(resourceId);
507
661
  (0, _utils3.deferDispatch)(function () {
508
- api === null || api === void 0 || api.core.actions.execute(function (_ref8) {
509
- var tr = _ref8.tr;
662
+ api === null || api === void 0 || api.core.actions.execute(function (_ref9) {
663
+ var tr = _ref9.tr;
510
664
  return tr.setMeta(syncedBlockPluginKey, {
511
665
  activeFlag: {
512
666
  id: _types.FLAG_ID.UNPUBLISHED_SYNC_BLOCK_PASTED,
@@ -818,7 +972,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
818
972
  return false;
819
973
  },
820
974
  cut: function cut() {
821
- if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_13')) {
975
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_13') || Boolean(options === null || options === void 0 ? void 0 : options.onGiveFeedback) && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true)) {
822
976
  ctx.consumeCutEvent();
823
977
  ctx.markCutEvent();
824
978
  }
@@ -849,27 +1003,47 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
849
1003
  return node;
850
1004
  });
851
1005
  },
852
- transformCopied: function transformCopied(slice, _ref9) {
853
- var state = _ref9.state;
1006
+ transformCopied: function transformCopied(slice, _ref0) {
1007
+ var state = _ref0.state;
854
1008
  var pluginState = syncedBlockPluginKey.getState(state);
855
1009
  var syncBlockStore = pluginState === null || pluginState === void 0 ? void 0 : pluginState.syncBlockStore;
856
1010
  var schema = state.schema;
857
1011
  var isCopy = ctx.consumeCopyEvent();
858
1012
  var isSyncedBlockPatch13Enabled = (0, _platformFeatureFlags.fg)('platform_synced_block_patch_13');
859
- var isCut = isSyncedBlockPatch13Enabled && ctx.consumeCutEvent();
860
- if (!syncBlockStore || !isCopy && !isCut) {
1013
+ var wasCut = ctx.consumeCutEvent();
1014
+ var isCut = isSyncedBlockPatch13Enabled && wasCut;
1015
+ var isPromptedFeedbackCut = Boolean(syncBlockStore && (options === null || options === void 0 ? void 0 : options.onGiveFeedback) && wasCut) && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true);
1016
+ if (!syncBlockStore || !isCopy && !isCut && !isPromptedFeedbackCut) {
861
1017
  return slice;
862
1018
  }
863
- return (0, _utils.mapSlice)(slice, function (node) {
1019
+ if (isPromptedFeedbackCut && !isCut) {
1020
+ var _containsCutSyncBlock = false;
1021
+ (0, _utils.mapSlice)(slice, function (node) {
1022
+ if (syncBlockStore.referenceManager.isReferenceBlock(node) || node.type.name === 'bodiedSyncBlock' && (0, _utils3.sliceFullyContainsNode)(slice, node)) {
1023
+ _containsCutSyncBlock = true;
1024
+ }
1025
+ return node;
1026
+ });
1027
+ if (_containsCutSyncBlock) {
1028
+ ctx.markCutRemovalPending();
1029
+ }
1030
+ return slice;
1031
+ }
1032
+ var containsCutSyncBlock = false;
1033
+ var transformedSlice = (0, _utils.mapSlice)(slice, function (node) {
864
1034
  if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
865
1035
  var _syncBlockStore$refer;
866
1036
  if (isCut) {
1037
+ containsCutSyncBlock = isPromptedFeedbackCut;
867
1038
  return node;
868
1039
  }
869
1040
  showCopiedFlag(api, options === null || options === void 0 ? void 0 : options.__livePage, ((_syncBlockStore$refer = syncBlockStore.referenceManager.getFromCache(node.attrs.resourceId)) === null || _syncBlockStore$refer === void 0 || (_syncBlockStore$refer = _syncBlockStore$refer.data) === null || _syncBlockStore$refer === void 0 ? void 0 : _syncBlockStore$refer.status) === 'unpublished', (0, _utils2.getSourceProductFromResourceIdSafe)(node.attrs.resourceId));
870
1041
  return node;
871
1042
  }
872
1043
  if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
1044
+ if (isPromptedFeedbackCut && isCut && (0, _utils3.sliceFullyContainsNode)(slice, node)) {
1045
+ containsCutSyncBlock = true;
1046
+ }
873
1047
  // if we only selected part of the bodied sync block content,
874
1048
  // remove the sync block node and only keep the content
875
1049
  if (!(0, _utils3.sliceFullyContainsNode)(slice, node)) {
@@ -893,6 +1067,10 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
893
1067
  }
894
1068
  return node;
895
1069
  });
1070
+ if (containsCutSyncBlock) {
1071
+ ctx.markCutRemovalPending();
1072
+ }
1073
+ return transformedSlice;
896
1074
  }
897
1075
  },
898
1076
  filterTransaction: function filterTransaction(tr, state) {
@@ -979,6 +1157,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
979
1157
  syncBlockStore: syncBlockStore,
980
1158
  api: api,
981
1159
  confirmationTransactionRef: confirmationTransactionRef,
1160
+ ctx: ctx,
982
1161
  bodiedSyncBlockRemoved: bodiedSyncBlockRemoved,
983
1162
  bodiedSyncBlockAdded: bodiedSyncBlockAdded,
984
1163
  extensionFlagShown: extensionFlagShown
@@ -1002,6 +1181,17 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
1002
1181
  if (viewMode === 'view') {
1003
1182
  return null;
1004
1183
  }
1184
+ if (options !== null && options !== void 0 && options.onGiveFeedback) {
1185
+ var feedbackContexts = trs.flatMap(function (tr) {
1186
+ var feedbackContext = tr.getMeta(syncedBlockPromptedFeedbackMetaKey);
1187
+ return feedbackContext ? [feedbackContext] : [];
1188
+ });
1189
+ if (feedbackContexts.length > 0 && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true)) {
1190
+ feedbackContexts.forEach(function (feedbackContext) {
1191
+ ctx.handleAppliedFeedback(feedbackContext);
1192
+ });
1193
+ }
1194
+ }
1005
1195
 
1006
1196
  // Update source sync block cache for user-initiated changes only.
1007
1197
  // Cache updates are handled here instead of in the nodeview update() so we
@@ -1086,8 +1276,8 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
1086
1276
  if (!(step instanceof _transform.ReplaceStep || step instanceof _transform.ReplaceAroundStep) || !('slice' in step)) {
1087
1277
  return false;
1088
1278
  }
1089
- var _ref0 = step,
1090
- slice = _ref0.slice;
1279
+ var _ref1 = step,
1280
+ slice = _ref1.slice;
1091
1281
  var found = false;
1092
1282
  slice.content.descendants(function (node) {
1093
1283
  if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
@@ -1127,8 +1317,8 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
1127
1317
  tr.setMeta('addToHistory', false);
1128
1318
  (0, _utils3.deferDispatch)(function () {
1129
1319
  var _api$core;
1130
- api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref1) {
1131
- var tr = _ref1.tr;
1320
+ api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref10) {
1321
+ var tr = _ref10.tr;
1132
1322
  return tr.setMeta(syncedBlockPluginKey, {
1133
1323
  activeFlag: {
1134
1324
  id: _types.FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK
@@ -34,7 +34,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
34
34
  });
35
35
  };
36
36
  };
37
- var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
37
+ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism, feedbackCallbacks) {
38
38
  // Clear potential old pending deletion to retreat the deletion as first attempt
39
39
  syncBlockStore.sourceManager.clearPendingDeletion();
40
40
 
@@ -70,6 +70,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
70
70
  return null;
71
71
  }
72
72
  recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
73
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(recomputedTr);
73
74
  if (!recomputedTr.getMeta(_utils.pmHistoryPluginKey)) {
74
75
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
75
76
  // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
@@ -85,6 +86,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
85
86
  }
86
87
  api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function () {
87
88
  var trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
89
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(trToDispatch);
88
90
  if (!trToDispatch.getMeta(_utils.pmHistoryPluginKey)) {
89
91
  // bodiedSyncBlock deletion is expected to be permanent (cannot undo)
90
92
  // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
@@ -94,6 +96,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
94
96
  });
95
97
  }, function (success) {
96
98
  var _api$core4;
99
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteCompleted(success);
97
100
  api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
98
101
  var tr = _ref3.tr;
99
102
  var newState;
@@ -121,6 +124,7 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
121
124
  });
122
125
  }, function () {
123
126
  confirmationTransactionRef.current = undefined;
127
+ feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDestroy();
124
128
  }, mechanism);
125
129
  return false;
126
130
  };
@@ -21,6 +21,7 @@ var _DeleteConfirmationModal = require("./ui/DeleteConfirmationModal");
21
21
  var _Flag = require("./ui/Flag");
22
22
  var _floatingToolbar = require("./ui/floating-toolbar");
23
23
  var _quickInsert = require("./ui/quick-insert");
24
+ var _SourceSyncBlockPlaceholder = require("./ui/SourceSyncBlockPlaceholder");
24
25
  var _SyncBlockRefresher = require("./ui/SyncBlockRefresher");
25
26
  var _toolbarComponents = require("./ui/toolbar-components");
26
27
  /**
@@ -157,10 +158,10 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
157
158
  refs.containerElement = containerElement || undefined;
158
159
  refs.popupsMountPoint = popupsMountPoint || undefined;
159
160
  refs.wrapperElement = wrapperElement || undefined;
160
- return /*#__PURE__*/_react.default.createElement(LazySyncedBlockUI, {
161
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, (0, _expValEquals.expValEquals)('platform_editor_sync_block_activation', 'isEnabled', true) && /*#__PURE__*/_react.default.createElement(_SourceSyncBlockPlaceholder.SourceSyncBlockPlaceholder, null), /*#__PURE__*/_react.default.createElement(LazySyncedBlockUI, {
161
162
  syncBlockStore: syncBlockStore,
162
163
  api: api
163
- });
164
+ }));
164
165
  },
165
166
  getSharedState: function getSharedState(editorState) {
166
167
  if (!editorState) {
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.SourceSyncBlockPlaceholder = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _react = require("react");
10
+ var _react2 = require("@emotion/react");
11
+ var _reactIntl = require("react-intl");
12
+ var _messages = require("@atlaskit/editor-common/messages");
13
+ var _syncBlock = require("@atlaskit/editor-common/sync-block");
14
+ /**
15
+ * @jsxRuntime classic
16
+ * @jsx jsx
17
+ */
18
+
19
+ // Plugin content components need a global selector because the ProseMirror content is not a child
20
+ // of this component. This follows the existing layout placeholder pattern.
21
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles, @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
22
+
23
+ var getSourceSyncBlockPlaceholderStyles = function getSourceSyncBlockPlaceholderStyles(placeholderText) {
24
+ return (0, _react2.css)((0, _defineProperty2.default)({}, ".ProseMirror .".concat(_syncBlock.BodiedSyncBlockSharedCssClassName.prefix, " .").concat(_syncBlock.BodiedSyncBlockSharedCssClassName.content, "[contenteditable=\"true\"] > p:only-child:has(.ProseMirror-trailingBreak:only-child)"), {
25
+ position: 'relative',
26
+ '&::before': {
27
+ // JSON.stringify produces a quoted and escaped CSS string.
28
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values
29
+ content: placeholderText,
30
+ position: 'absolute',
31
+ boxSizing: 'border-box',
32
+ color: "var(--ds-text-subtlest, #6B6E76)",
33
+ font: 'inherit',
34
+ insetInlineStart: 0,
35
+ width: '100%',
36
+ pointerEvents: 'none',
37
+ userSelect: 'none'
38
+ }
39
+ }));
40
+ };
41
+ var SourceSyncBlockPlaceholder = exports.SourceSyncBlockPlaceholder = function SourceSyncBlockPlaceholder() {
42
+ var _useIntl = (0, _reactIntl.useIntl)(),
43
+ formatMessage = _useIntl.formatMessage;
44
+ var placeholderStyles = (0, _react.useMemo)(function () {
45
+ return getSourceSyncBlockPlaceholderStyles(JSON.stringify(formatMessage(_messages.placeholderTextMessages.sourceSyncBlockPlaceholderText)));
46
+ }, [formatMessage]);
47
+ return (0, _react2.jsx)(_react2.Global, {
48
+ styles: placeholderStyles
49
+ });
50
+ };