@atlaskit/editor-plugin-synced-block 10.0.20 → 10.1.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/CHANGELOG.md +27 -0
- package/dist/cjs/editor-commands/index.js +15 -2
- package/dist/cjs/pm-plugins/main.js +87 -11
- package/dist/cjs/pm-plugins/utils/handle-bodied-sync-block-removal.js +42 -7
- package/dist/cjs/pm-plugins/utils/recompute-delete-transaction.js +98 -0
- package/dist/es2019/editor-commands/index.js +16 -2
- package/dist/es2019/pm-plugins/main.js +82 -9
- package/dist/es2019/pm-plugins/utils/handle-bodied-sync-block-removal.js +40 -6
- package/dist/es2019/pm-plugins/utils/recompute-delete-transaction.js +90 -0
- package/dist/esm/editor-commands/index.js +16 -3
- package/dist/esm/pm-plugins/main.js +87 -11
- package/dist/esm/pm-plugins/utils/handle-bodied-sync-block-removal.js +42 -7
- package/dist/esm/pm-plugins/utils/recompute-delete-transaction.js +92 -0
- package/dist/types/pm-plugins/main.d.ts +34 -0
- package/dist/types/pm-plugins/utils/handle-bodied-sync-block-removal.d.ts +3 -2
- package/dist/types/pm-plugins/utils/recompute-delete-transaction.d.ts +29 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-synced-block
|
|
2
2
|
|
|
3
|
+
## 10.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`ecafba7c81b7d`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ecafba7c81b7d) -
|
|
8
|
+
Enrich synced block deletion analytics so source-block deletions can be explained rather than just
|
|
9
|
+
counted. The source `syncedBlockDelete` operational event now carries `deletionReason` and a
|
|
10
|
+
`mechanism` dimension (undo/redo/deleteButton/keyboardDelete/selectionReplaced/other), and
|
|
11
|
+
repeated emissions for the same removal are de-duplicated. A real operational `syncedBlockCreate`
|
|
12
|
+
success event is now emitted, and the source bare-uuid join key is added to the copy event so
|
|
13
|
+
create/copy/delete can be correlated. All new attributes and events are gated behind
|
|
14
|
+
`platform_editor_blocks_patch_4`; gate-off behaviour is unchanged.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
|
|
20
|
+
## 10.0.21
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- [`845de90a5f39e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/845de90a5f39e) -
|
|
25
|
+
[ux] Fix source synced block deletion intermittently failing when the document changes (from your
|
|
26
|
+
own edits or a collaborator's) while the delete confirmation dialog is open. The delete is now
|
|
27
|
+
recomputed from the current document on confirm, instead of replaying a stale transaction captured
|
|
28
|
+
when the dialog opened. Gated behind `platform_editor_blocks_patch_4`.
|
|
29
|
+
|
|
3
30
|
## 10.0.20
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
|
@@ -158,6 +158,10 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
|
|
|
158
158
|
}
|
|
159
159
|
var domNode = (0, _copyButton.toDOM)(referenceSyncBlockNode, schema);
|
|
160
160
|
(0, _copyButton.copyDomNode)(domNode, referenceSyncBlockNode.type, selection);
|
|
161
|
+
|
|
162
|
+
// Bare-uuid join key shared with the create/delete events: for a source
|
|
163
|
+
// bodiedSyncBlock its `localId` is the source uuid (copy was page-form only).
|
|
164
|
+
var sourceJoinKey = isBodiedSyncBlock ? syncBlockFindResult.node.attrs.localId : undefined;
|
|
161
165
|
(0, _utils2.deferDispatch)(function () {
|
|
162
166
|
api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
|
|
163
167
|
var _api$analytics4;
|
|
@@ -167,10 +171,12 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
|
|
|
167
171
|
action: _analytics.ACTION.COPIED,
|
|
168
172
|
actionSubject: _analytics.ACTION_SUBJECT.SYNCED_BLOCK,
|
|
169
173
|
actionSubjectId: _analytics.ACTION_SUBJECT_ID.SYNCED_BLOCK_COPY,
|
|
170
|
-
attributes: {
|
|
174
|
+
attributes: _objectSpread({
|
|
171
175
|
resourceId: referenceSyncBlockNode.attrs.resourceId,
|
|
172
176
|
inputMethod: inputMethod
|
|
173
|
-
}
|
|
177
|
+
}, sourceJoinKey && (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4') && {
|
|
178
|
+
blockInstanceId: sourceJoinKey
|
|
179
|
+
})
|
|
174
180
|
});
|
|
175
181
|
return tr.setMeta(_main.syncedBlockPluginKey, {
|
|
176
182
|
activeFlag: {
|
|
@@ -246,6 +252,13 @@ var removeSyncedBlock = exports.removeSyncedBlock = function removeSyncedBlock(a
|
|
|
246
252
|
if (!removeTr) {
|
|
247
253
|
return false;
|
|
248
254
|
}
|
|
255
|
+
|
|
256
|
+
// Tag the transaction so analytics can report this as `deleteButton` rather
|
|
257
|
+
// than a keyboard delete (both produce a plain ReplaceStep). Gated so the
|
|
258
|
+
// gate-off transaction is unchanged.
|
|
259
|
+
if ((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
|
|
260
|
+
removeTr.setMeta(_main.deleteMechanismMetaKey, 'deleteButton');
|
|
261
|
+
}
|
|
249
262
|
dispatch(removeTr);
|
|
250
263
|
api === null || api === void 0 || api.core.actions.focus();
|
|
251
264
|
return true;
|
|
@@ -4,10 +4,11 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
-
exports.syncedBlockPluginKey = exports.createPlugin = void 0;
|
|
7
|
+
exports.syncedBlockPluginKey = exports.getDeleteMechanism = exports.deleteMechanismMetaKey = 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"));
|
|
11
|
+
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
|
|
11
12
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
12
13
|
var _analytics = require("@atlaskit/editor-common/analytics");
|
|
13
14
|
var _collab = require("@atlaskit/editor-common/collab");
|
|
@@ -41,6 +42,17 @@ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol
|
|
|
41
42
|
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
42
43
|
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
43
44
|
var syncedBlockPluginKey = exports.syncedBlockPluginKey = new _state.PluginKey('syncedBlockPlugin');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Dedicated transaction-meta {@link PluginKey} the synced-block removal command
|
|
48
|
+
* sets so {@link getDeleteMechanism} can report a toolbar Delete as
|
|
49
|
+
* `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
|
|
50
|
+
* plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
|
|
51
|
+
* carries plugin-state transitions read in `apply()`, or a bare string) keeps
|
|
52
|
+
* this transient signal uniquely namespaced and isolated. Only read behind
|
|
53
|
+
* `platform_editor_blocks_patch_4`.
|
|
54
|
+
*/
|
|
55
|
+
var deleteMechanismMetaKey = exports.deleteMechanismMetaKey = new _state.PluginKey('syncedBlockDeleteMechanism');
|
|
44
56
|
var mapRetryCreationPosMap = function mapRetryCreationPosMap(oldMap, newRetryCreationPos, mapPos, isPerfExperimentOn) {
|
|
45
57
|
var resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;
|
|
46
58
|
|
|
@@ -121,6 +133,55 @@ var getDeleteReason = function getDeleteReason(tr) {
|
|
|
121
133
|
}
|
|
122
134
|
return reason;
|
|
123
135
|
};
|
|
136
|
+
|
|
137
|
+
/** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
|
|
138
|
+
var isHistoryMeta = function isHistoryMeta(meta) {
|
|
139
|
+
return (0, _typeof2.default)(meta) === 'object' && meta !== null && 'redo' in meta;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
|
|
144
|
+
* analytics dimension. Each value names the user action; most-specific wins:
|
|
145
|
+
* - `undo` / `redo` — history transaction.
|
|
146
|
+
* - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
|
|
147
|
+
* (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
|
|
148
|
+
* - `selectionReplaced` — ReplaceStep on a node-selected block (the
|
|
149
|
+
* accidental-overwrite path: block selected, then typed/pasted over).
|
|
150
|
+
* - `keyboardDelete` — ReplaceStep removal at a caret/range selection
|
|
151
|
+
* (Backspace/Delete key).
|
|
152
|
+
* - `other` — anything else (unsync, conversion, code-dispatched, or a
|
|
153
|
+
* structural `ReplaceAroundStep` such as wrap/lift/unwrap).
|
|
154
|
+
*
|
|
155
|
+
* Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
|
|
156
|
+
* is used for wrapping/lifting/unwrapping content, not direct removal, so a
|
|
157
|
+
* removal carried by one is classified as `other` rather than misreported as a
|
|
158
|
+
* keyboard delete.
|
|
159
|
+
*
|
|
160
|
+
* `state` is the pre-transaction state, so its selection reflects what was
|
|
161
|
+
* selected when the edit was made.
|
|
162
|
+
*/
|
|
163
|
+
var getDeleteMechanism = exports.getDeleteMechanism = function getDeleteMechanism(tr, state) {
|
|
164
|
+
var _selection$node;
|
|
165
|
+
var historyMeta = tr.getMeta(_utils.pmHistoryPluginKey);
|
|
166
|
+
if (historyMeta) {
|
|
167
|
+
return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The toolbar Delete control tags its transaction so we can report it
|
|
171
|
+
// distinctly from a keyboard delete (both produce a plain ReplaceStep).
|
|
172
|
+
if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
|
|
173
|
+
return 'deleteButton';
|
|
174
|
+
}
|
|
175
|
+
var hasReplaceStep = tr.steps.some(function (step) {
|
|
176
|
+
return step instanceof _transform.ReplaceStep;
|
|
177
|
+
});
|
|
178
|
+
if (!hasReplaceStep) {
|
|
179
|
+
return 'other';
|
|
180
|
+
}
|
|
181
|
+
var selection = state.selection;
|
|
182
|
+
var isNodeSelected = selection instanceof _state.NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
|
|
183
|
+
return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
|
|
184
|
+
};
|
|
124
185
|
var filterTransactionOnline = function filterTransactionOnline(_ref3) {
|
|
125
186
|
var _tr$getMeta;
|
|
126
187
|
var tr = _ref3.tr,
|
|
@@ -171,9 +232,18 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
|
|
|
171
232
|
});
|
|
172
233
|
});
|
|
173
234
|
if (bodiedSyncBlockRemoved.length > 0) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
235
|
+
if (!(0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
|
|
236
|
+
// Legacy stash-and-rebase path: store the original delete transaction so it
|
|
237
|
+
// can be rebased against intervening edits and replayed on confirm. When
|
|
238
|
+
// platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
|
|
239
|
+
// from the live document on confirm (see handleBodiedSyncBlockRemoval), so
|
|
240
|
+
// there is no need to stash the transaction here. See EDITOR-7889.
|
|
241
|
+
// eslint-disable-next-line no-param-reassign
|
|
242
|
+
confirmationTransactionRef.current = tr;
|
|
243
|
+
}
|
|
244
|
+
// Only derive the mechanism behind the gate — discarded gate-off.
|
|
245
|
+
var mechanism = (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
|
|
246
|
+
return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
|
|
177
247
|
}
|
|
178
248
|
if (bodiedSyncBlockAdded.length > 0) {
|
|
179
249
|
if (tr.getMeta(_utils.pmHistoryPluginKey)) {
|
|
@@ -887,13 +957,19 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
|
|
|
887
957
|
}
|
|
888
958
|
});
|
|
889
959
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
960
|
+
|
|
961
|
+
// Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
|
|
962
|
+
// delete is recomputed fresh from the live document on confirm, so there is
|
|
963
|
+
// no stashed transaction to rebase here. See EDITOR-7889.
|
|
964
|
+
if (!(0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
|
|
965
|
+
trs.filter(function (tr) {
|
|
966
|
+
return tr.docChanged;
|
|
967
|
+
}).forEach(function (tr) {
|
|
968
|
+
if (confirmationTransactionRef.current) {
|
|
969
|
+
confirmationTransactionRef.current = (0, _editorSyncedBlockProvider.rebaseTransaction)(confirmationTransactionRef.current, tr, newState);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
897
973
|
var _iterator2 = _createForOfIteratorHelper(trs),
|
|
898
974
|
_step2;
|
|
899
975
|
try {
|
|
@@ -7,8 +7,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
7
7
|
exports.handleBodiedSyncBlockRemoval = void 0;
|
|
8
8
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
9
|
var _utils = require("@atlaskit/editor-common/utils");
|
|
10
|
+
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
10
11
|
var _types = require("../../types");
|
|
11
12
|
var _main = require("../main");
|
|
13
|
+
var _recomputeDeleteTransaction = require("./recompute-delete-transaction");
|
|
12
14
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
13
15
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
14
16
|
var onRetry = function onRetry(api, syncBlockStore) {
|
|
@@ -32,7 +34,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
|
|
|
32
34
|
});
|
|
33
35
|
};
|
|
34
36
|
};
|
|
35
|
-
var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) {
|
|
37
|
+
var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
|
|
36
38
|
// Clear potential old pending deletion to retreat the deletion as first attempt
|
|
37
39
|
syncBlockStore.sourceManager.clearPendingDeletion();
|
|
38
40
|
|
|
@@ -43,12 +45,45 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
|
|
|
43
45
|
syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(function (node) {
|
|
44
46
|
return node.attrs;
|
|
45
47
|
}), deletionReason, function () {
|
|
46
|
-
var _api$
|
|
48
|
+
var _api$core3;
|
|
49
|
+
if ((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
|
|
50
|
+
var _api$core2;
|
|
51
|
+
// Recompute the delete fresh from the live document instead of
|
|
52
|
+
// replaying a stashed-and-rebased transaction. The stash/rebase
|
|
53
|
+
// approach produced stale, schema-invalid transactions when the
|
|
54
|
+
// document changed shape (local edits or remote collab) while the
|
|
55
|
+
// confirmation modal was open — the dominant signature being
|
|
56
|
+
// "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
|
|
57
|
+
api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
|
|
58
|
+
var tr = _ref2.tr;
|
|
59
|
+
var recomputedTr = (0, _recomputeDeleteTransaction.recomputeDeleteTransaction)(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
|
|
60
|
+
return node.attrs;
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// The target node(s) no longer exist in the live document (e.g.
|
|
64
|
+
// a remote collaborator already removed them). There is nothing
|
|
65
|
+
// to delete locally, so return `null` to skip the dispatch
|
|
66
|
+
// entirely — dispatching the untouched transaction would be a
|
|
67
|
+
// no-op that still runs the plugin's filter/append hooks. The
|
|
68
|
+
// backend deletion has already been issued by the store manager.
|
|
69
|
+
if (!recomputedTr) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
|
|
73
|
+
if (!recomputedTr.getMeta(_utils.pmHistoryPluginKey)) {
|
|
74
|
+
// bodiedSyncBlock deletion is expected to be permanent (cannot undo)
|
|
75
|
+
// For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
|
|
76
|
+
recomputedTr.setMeta('addToHistory', false);
|
|
77
|
+
}
|
|
78
|
+
return recomputedTr;
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
47
82
|
var confirmationTransaction = confirmationTransactionRef.current;
|
|
48
83
|
if (!confirmationTransaction) {
|
|
49
84
|
return;
|
|
50
85
|
}
|
|
51
|
-
api === null || api === void 0 || (_api$
|
|
86
|
+
api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function () {
|
|
52
87
|
var trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
|
|
53
88
|
if (!trToDispatch.getMeta(_utils.pmHistoryPluginKey)) {
|
|
54
89
|
// bodiedSyncBlock deletion is expected to be permanent (cannot undo)
|
|
@@ -58,9 +93,9 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
|
|
|
58
93
|
return trToDispatch;
|
|
59
94
|
});
|
|
60
95
|
}, function (success) {
|
|
61
|
-
var _api$
|
|
62
|
-
api === null || api === void 0 || (_api$
|
|
63
|
-
var tr =
|
|
96
|
+
var _api$core4;
|
|
97
|
+
api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
|
|
98
|
+
var tr = _ref3.tr;
|
|
64
99
|
var newState;
|
|
65
100
|
if (!success) {
|
|
66
101
|
newState = {
|
|
@@ -86,6 +121,6 @@ var handleBodiedSyncBlockRemoval = exports.handleBodiedSyncBlockRemoval = functi
|
|
|
86
121
|
});
|
|
87
122
|
}, function () {
|
|
88
123
|
confirmationTransactionRef.current = undefined;
|
|
89
|
-
});
|
|
124
|
+
}, mechanism);
|
|
90
125
|
return false;
|
|
91
126
|
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.recomputeDeleteTransaction = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Whether `node` is the source block requested by `wanted`.
|
|
9
|
+
*
|
|
10
|
+
* Matching is done *per requested block* (rather than against global pools of
|
|
11
|
+
* every requested localId/resourceId) so a node cannot be matched by combining
|
|
12
|
+
* one block's `localId` with a different block's `resourceId`. We match on the
|
|
13
|
+
* requested block's `localId` first and only fall back to its `resourceId` when
|
|
14
|
+
* the `localId` has been regenerated (so it no longer matches the live node).
|
|
15
|
+
*/
|
|
16
|
+
var matchesRequestedBlock = function matchesRequestedBlock(node, wanted) {
|
|
17
|
+
var _ref = node.attrs,
|
|
18
|
+
localId = _ref.localId,
|
|
19
|
+
resourceId = _ref.resourceId;
|
|
20
|
+
if (wanted.localId && localId && wanted.localId === localId) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return Boolean(wanted.resourceId && resourceId && wanted.resourceId === resourceId);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Locate the live positions of the source `bodiedSyncBlock` nodes identified by
|
|
28
|
+
* `syncBlockIds`, matching first on `localId` and falling back to `resourceId`.
|
|
29
|
+
*
|
|
30
|
+
* This walks the *current* document (`tr.doc`) rather than relying on positions
|
|
31
|
+
* captured when the delete was first triggered, so it is robust to intervening
|
|
32
|
+
* local edits and remote collab changes that occur while the confirmation modal
|
|
33
|
+
* is open.
|
|
34
|
+
*/
|
|
35
|
+
var findSourceBlocks = function findSourceBlocks(tr, isSourceBlock, syncBlockIds) {
|
|
36
|
+
var matches = [];
|
|
37
|
+
tr.doc.descendants(function (node, pos) {
|
|
38
|
+
if (!isSourceBlock(node)) {
|
|
39
|
+
// bodiedSyncBlock is always a top-level node, so there is no need to
|
|
40
|
+
// descend into other branches looking for one.
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (syncBlockIds.some(function (wanted) {
|
|
44
|
+
return matchesRequestedBlock(node, wanted);
|
|
45
|
+
})) {
|
|
46
|
+
matches.push({
|
|
47
|
+
node: node,
|
|
48
|
+
pos: pos
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Never recurse into a source block's body.
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
return matches;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Recompute a source `bodiedSyncBlock` delete from the live document.
|
|
60
|
+
*
|
|
61
|
+
* Instead of replaying a transaction that was stashed when the delete was first
|
|
62
|
+
* triggered (and then manually rebased against every intervening edit — the
|
|
63
|
+
* fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>"
|
|
64
|
+
* and assorted position/open-depth errors), this finds the target node(s) by
|
|
65
|
+
* `localId`/`resourceId` in the current state and issues a fresh
|
|
66
|
+
* `tr.delete(pos, pos + nodeSize)` for each.
|
|
67
|
+
*
|
|
68
|
+
* Deletes are applied in reverse document order so earlier deletes do not
|
|
69
|
+
* invalidate the positions of later ones.
|
|
70
|
+
*
|
|
71
|
+
* Partial matches are handled gracefully: when only some of the requested
|
|
72
|
+
* blocks are still present (e.g. a remote collaborator removed the rest while
|
|
73
|
+
* the confirmation modal was open), the found ones are deleted and the missing
|
|
74
|
+
* ones are skipped. This is intentional — the backend deletion for every
|
|
75
|
+
* requested block has already been issued by the store manager, so the local
|
|
76
|
+
* transaction only needs to remove whatever is still in the live document.
|
|
77
|
+
*
|
|
78
|
+
* @returns the mutated transaction when at least one target node was found and
|
|
79
|
+
* deleted (this may be a partial delete if some targets were already gone),
|
|
80
|
+
* otherwise `undefined` when none of the targets exist any more — e.g. a remote
|
|
81
|
+
* collaborator already removed them all — so there is nothing to delete.
|
|
82
|
+
*/
|
|
83
|
+
var recomputeDeleteTransaction = exports.recomputeDeleteTransaction = function recomputeDeleteTransaction(tr, isSourceBlock, syncBlockIds) {
|
|
84
|
+
var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
|
|
85
|
+
if (matches.length === 0) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Delete in reverse document order so positions remain valid across deletes.
|
|
90
|
+
matches.sort(function (a, b) {
|
|
91
|
+
return b.pos - a.pos;
|
|
92
|
+
}).forEach(function (_ref2) {
|
|
93
|
+
var node = _ref2.node,
|
|
94
|
+
pos = _ref2.pos;
|
|
95
|
+
tr.delete(pos, pos + node.nodeSize);
|
|
96
|
+
});
|
|
97
|
+
return tr;
|
|
98
|
+
};
|
|
@@ -5,7 +5,7 @@ import { DOMSerializer, Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
|
5
5
|
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
6
6
|
import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
|
|
7
7
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
8
|
-
import { syncedBlockPluginKey } from '../pm-plugins/main';
|
|
8
|
+
import { deleteMechanismMetaKey, syncedBlockPluginKey } from '../pm-plugins/main';
|
|
9
9
|
import { canBeConvertedToSyncBlock, deferDispatch, findSyncBlock, findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
|
|
10
10
|
import { FLAG_ID } from '../types';
|
|
11
11
|
import { findBodiedSyncBlockByLocalId, pasteSyncBlockHTMLContent } from './utils';
|
|
@@ -153,6 +153,10 @@ const copySyncedBlockReferenceToClipboardInternal = (schema, selection, syncBloc
|
|
|
153
153
|
}
|
|
154
154
|
const domNode = toDOM(referenceSyncBlockNode, schema);
|
|
155
155
|
copyDomNode(domNode, referenceSyncBlockNode.type, selection);
|
|
156
|
+
|
|
157
|
+
// Bare-uuid join key shared with the create/delete events: for a source
|
|
158
|
+
// bodiedSyncBlock its `localId` is the source uuid (copy was page-form only).
|
|
159
|
+
const sourceJoinKey = isBodiedSyncBlock ? syncBlockFindResult.node.attrs.localId : undefined;
|
|
156
160
|
deferDispatch(() => {
|
|
157
161
|
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
|
|
158
162
|
tr
|
|
@@ -165,7 +169,10 @@ const copySyncedBlockReferenceToClipboardInternal = (schema, selection, syncBloc
|
|
|
165
169
|
actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_COPY,
|
|
166
170
|
attributes: {
|
|
167
171
|
resourceId: referenceSyncBlockNode.attrs.resourceId,
|
|
168
|
-
inputMethod
|
|
172
|
+
inputMethod,
|
|
173
|
+
...(sourceJoinKey && fg('platform_editor_blocks_patch_4') && {
|
|
174
|
+
blockInstanceId: sourceJoinKey
|
|
175
|
+
})
|
|
169
176
|
}
|
|
170
177
|
});
|
|
171
178
|
return tr.setMeta(syncedBlockPluginKey, {
|
|
@@ -246,6 +253,13 @@ export const removeSyncedBlock = api => (state, dispatch, _view) => {
|
|
|
246
253
|
if (!removeTr) {
|
|
247
254
|
return false;
|
|
248
255
|
}
|
|
256
|
+
|
|
257
|
+
// Tag the transaction so analytics can report this as `deleteButton` rather
|
|
258
|
+
// than a keyboard delete (both produce a plain ReplaceStep). Gated so the
|
|
259
|
+
// gate-off transaction is unchanged.
|
|
260
|
+
if (fg('platform_editor_blocks_patch_4')) {
|
|
261
|
+
removeTr.setMeta(deleteMechanismMetaKey, 'deleteButton');
|
|
262
|
+
}
|
|
249
263
|
dispatch(removeTr);
|
|
250
264
|
api === null || api === void 0 ? void 0 : api.core.actions.focus();
|
|
251
265
|
return true;
|
|
@@ -6,7 +6,7 @@ import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
|
|
|
6
6
|
import { BodiedSyncBlockSharedCssClassName, SyncBlockStateCssClassName } from '@atlaskit/editor-common/sync-block';
|
|
7
7
|
import { mapSlice, pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
|
|
8
8
|
import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
|
|
9
|
-
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
9
|
+
import { NodeSelection, PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
10
10
|
import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
|
|
11
11
|
import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
12
12
|
import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
|
|
@@ -26,6 +26,17 @@ import { hasEditInSyncBlock, trackSyncBlocks } from './utils/track-sync-blocks';
|
|
|
26
26
|
import { transactionInsertsSyncedBlock } from './utils/transaction-inserts-synced-block';
|
|
27
27
|
import { deferDispatch, wasExtensionInsertedInBodiedSyncBlock, sliceFullyContainsNode } from './utils/utils';
|
|
28
28
|
export const syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Dedicated transaction-meta {@link PluginKey} the synced-block removal command
|
|
32
|
+
* sets so {@link getDeleteMechanism} can report a toolbar Delete as
|
|
33
|
+
* `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
|
|
34
|
+
* plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
|
|
35
|
+
* carries plugin-state transitions read in `apply()`, or a bare string) keeps
|
|
36
|
+
* this transient signal uniquely namespaced and isolated. Only read behind
|
|
37
|
+
* `platform_editor_blocks_patch_4`.
|
|
38
|
+
*/
|
|
39
|
+
export const deleteMechanismMetaKey = new PluginKey('syncedBlockDeleteMechanism');
|
|
29
40
|
const mapRetryCreationPosMap = (oldMap, newRetryCreationPos, mapPos, isPerfExperimentOn) => {
|
|
30
41
|
const resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;
|
|
31
42
|
|
|
@@ -95,6 +106,53 @@ const getDeleteReason = tr => {
|
|
|
95
106
|
}
|
|
96
107
|
return reason;
|
|
97
108
|
};
|
|
109
|
+
|
|
110
|
+
/** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
|
|
111
|
+
const isHistoryMeta = meta => typeof meta === 'object' && meta !== null && 'redo' in meta;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
|
|
115
|
+
* analytics dimension. Each value names the user action; most-specific wins:
|
|
116
|
+
* - `undo` / `redo` — history transaction.
|
|
117
|
+
* - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
|
|
118
|
+
* (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
|
|
119
|
+
* - `selectionReplaced` — ReplaceStep on a node-selected block (the
|
|
120
|
+
* accidental-overwrite path: block selected, then typed/pasted over).
|
|
121
|
+
* - `keyboardDelete` — ReplaceStep removal at a caret/range selection
|
|
122
|
+
* (Backspace/Delete key).
|
|
123
|
+
* - `other` — anything else (unsync, conversion, code-dispatched, or a
|
|
124
|
+
* structural `ReplaceAroundStep` such as wrap/lift/unwrap).
|
|
125
|
+
*
|
|
126
|
+
* Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
|
|
127
|
+
* is used for wrapping/lifting/unwrapping content, not direct removal, so a
|
|
128
|
+
* removal carried by one is classified as `other` rather than misreported as a
|
|
129
|
+
* keyboard delete.
|
|
130
|
+
*
|
|
131
|
+
* `state` is the pre-transaction state, so its selection reflects what was
|
|
132
|
+
* selected when the edit was made.
|
|
133
|
+
*/
|
|
134
|
+
export const getDeleteMechanism = (tr, state) => {
|
|
135
|
+
var _selection$node;
|
|
136
|
+
const historyMeta = tr.getMeta(pmHistoryPluginKey);
|
|
137
|
+
if (historyMeta) {
|
|
138
|
+
return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The toolbar Delete control tags its transaction so we can report it
|
|
142
|
+
// distinctly from a keyboard delete (both produce a plain ReplaceStep).
|
|
143
|
+
if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
|
|
144
|
+
return 'deleteButton';
|
|
145
|
+
}
|
|
146
|
+
const hasReplaceStep = tr.steps.some(step => step instanceof ReplaceStep);
|
|
147
|
+
if (!hasReplaceStep) {
|
|
148
|
+
return 'other';
|
|
149
|
+
}
|
|
150
|
+
const {
|
|
151
|
+
selection
|
|
152
|
+
} = state;
|
|
153
|
+
const isNodeSelected = selection instanceof NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
|
|
154
|
+
return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
|
|
155
|
+
};
|
|
98
156
|
const filterTransactionOnline = ({
|
|
99
157
|
tr,
|
|
100
158
|
state,
|
|
@@ -147,9 +205,18 @@ const filterTransactionOnline = ({
|
|
|
147
205
|
});
|
|
148
206
|
});
|
|
149
207
|
if (bodiedSyncBlockRemoved.length > 0) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
if (!fg('platform_editor_blocks_patch_4')) {
|
|
209
|
+
// Legacy stash-and-rebase path: store the original delete transaction so it
|
|
210
|
+
// can be rebased against intervening edits and replayed on confirm. When
|
|
211
|
+
// platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
|
|
212
|
+
// from the live document on confirm (see handleBodiedSyncBlockRemoval), so
|
|
213
|
+
// there is no need to stash the transaction here. See EDITOR-7889.
|
|
214
|
+
// eslint-disable-next-line no-param-reassign
|
|
215
|
+
confirmationTransactionRef.current = tr;
|
|
216
|
+
}
|
|
217
|
+
// Only derive the mechanism behind the gate — discarded gate-off.
|
|
218
|
+
const mechanism = fg('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
|
|
219
|
+
return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
|
|
153
220
|
}
|
|
154
221
|
if (bodiedSyncBlockAdded.length > 0) {
|
|
155
222
|
if (tr.getMeta(pmHistoryPluginKey)) {
|
|
@@ -844,11 +911,17 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
844
911
|
}
|
|
845
912
|
});
|
|
846
913
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
914
|
+
|
|
915
|
+
// Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
|
|
916
|
+
// delete is recomputed fresh from the live document on confirm, so there is
|
|
917
|
+
// no stashed transaction to rebase here. See EDITOR-7889.
|
|
918
|
+
if (!fg('platform_editor_blocks_patch_4')) {
|
|
919
|
+
trs.filter(tr => tr.docChanged).forEach(tr => {
|
|
920
|
+
if (confirmationTransactionRef.current) {
|
|
921
|
+
confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
852
925
|
for (const tr of trs) {
|
|
853
926
|
if (tr.getMeta(pmHistoryPluginKey)) {
|
|
854
927
|
const {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
|
|
2
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
2
3
|
import { FLAG_ID } from '../../types';
|
|
3
4
|
import { syncedBlockPluginKey } from '../main';
|
|
5
|
+
import { recomputeDeleteTransaction } from './recompute-delete-transaction';
|
|
4
6
|
const onRetry = (api, syncBlockStore) => () => {
|
|
5
7
|
var _api$core;
|
|
6
8
|
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
|
|
@@ -19,7 +21,7 @@ const onDismissed = syncBlockStore => tr => {
|
|
|
19
21
|
bodiedSyncBlockDeletionStatus: 'none'
|
|
20
22
|
});
|
|
21
23
|
};
|
|
22
|
-
export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) => {
|
|
24
|
+
export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) => {
|
|
23
25
|
// Clear potential old pending deletion to retreat the deletion as first attempt
|
|
24
26
|
syncBlockStore.sourceManager.clearPendingDeletion();
|
|
25
27
|
|
|
@@ -28,12 +30,44 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
|
|
|
28
30
|
// See editor-common/src/sync-block/sync-block-store-manager.ts for how we handle user confirmation and
|
|
29
31
|
// proceed with deletion.
|
|
30
32
|
syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(node => node.attrs), deletionReason, () => {
|
|
31
|
-
var _api$
|
|
33
|
+
var _api$core3;
|
|
34
|
+
if (fg('platform_editor_blocks_patch_4')) {
|
|
35
|
+
var _api$core2;
|
|
36
|
+
// Recompute the delete fresh from the live document instead of
|
|
37
|
+
// replaying a stashed-and-rebased transaction. The stash/rebase
|
|
38
|
+
// approach produced stale, schema-invalid transactions when the
|
|
39
|
+
// document changed shape (local edits or remote collab) while the
|
|
40
|
+
// confirmation modal was open — the dominant signature being
|
|
41
|
+
// "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
|
|
42
|
+
api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
|
|
43
|
+
tr
|
|
44
|
+
}) => {
|
|
45
|
+
const recomputedTr = recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(node => node.attrs));
|
|
46
|
+
|
|
47
|
+
// The target node(s) no longer exist in the live document (e.g.
|
|
48
|
+
// a remote collaborator already removed them). There is nothing
|
|
49
|
+
// to delete locally, so return `null` to skip the dispatch
|
|
50
|
+
// entirely — dispatching the untouched transaction would be a
|
|
51
|
+
// no-op that still runs the plugin's filter/append hooks. The
|
|
52
|
+
// backend deletion has already been issued by the store manager.
|
|
53
|
+
if (!recomputedTr) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
|
|
57
|
+
if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
|
|
58
|
+
// bodiedSyncBlock deletion is expected to be permanent (cannot undo)
|
|
59
|
+
// For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
|
|
60
|
+
recomputedTr.setMeta('addToHistory', false);
|
|
61
|
+
}
|
|
62
|
+
return recomputedTr;
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
32
66
|
const confirmationTransaction = confirmationTransactionRef.current;
|
|
33
67
|
if (!confirmationTransaction) {
|
|
34
68
|
return;
|
|
35
69
|
}
|
|
36
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
70
|
+
api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(() => {
|
|
37
71
|
const trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
|
|
38
72
|
if (!trToDispatch.getMeta(pmHistoryPluginKey)) {
|
|
39
73
|
// bodiedSyncBlock deletion is expected to be permanent (cannot undo)
|
|
@@ -43,8 +77,8 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
|
|
|
43
77
|
return trToDispatch;
|
|
44
78
|
});
|
|
45
79
|
}, success => {
|
|
46
|
-
var _api$
|
|
47
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
80
|
+
var _api$core4;
|
|
81
|
+
api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions.execute(({
|
|
48
82
|
tr
|
|
49
83
|
}) => {
|
|
50
84
|
let newState;
|
|
@@ -73,6 +107,6 @@ export const handleBodiedSyncBlockRemoval = (bodiedSyncBlockRemoved, syncBlockSt
|
|
|
73
107
|
});
|
|
74
108
|
}, () => {
|
|
75
109
|
confirmationTransactionRef.current = undefined;
|
|
76
|
-
});
|
|
110
|
+
}, mechanism);
|
|
77
111
|
return false;
|
|
78
112
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `node` is the source block requested by `wanted`.
|
|
3
|
+
*
|
|
4
|
+
* Matching is done *per requested block* (rather than against global pools of
|
|
5
|
+
* every requested localId/resourceId) so a node cannot be matched by combining
|
|
6
|
+
* one block's `localId` with a different block's `resourceId`. We match on the
|
|
7
|
+
* requested block's `localId` first and only fall back to its `resourceId` when
|
|
8
|
+
* the `localId` has been regenerated (so it no longer matches the live node).
|
|
9
|
+
*/
|
|
10
|
+
const matchesRequestedBlock = (node, wanted) => {
|
|
11
|
+
const {
|
|
12
|
+
localId,
|
|
13
|
+
resourceId
|
|
14
|
+
} = node.attrs;
|
|
15
|
+
if (wanted.localId && localId && wanted.localId === localId) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return Boolean(wanted.resourceId && resourceId && wanted.resourceId === resourceId);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Locate the live positions of the source `bodiedSyncBlock` nodes identified by
|
|
23
|
+
* `syncBlockIds`, matching first on `localId` and falling back to `resourceId`.
|
|
24
|
+
*
|
|
25
|
+
* This walks the *current* document (`tr.doc`) rather than relying on positions
|
|
26
|
+
* captured when the delete was first triggered, so it is robust to intervening
|
|
27
|
+
* local edits and remote collab changes that occur while the confirmation modal
|
|
28
|
+
* is open.
|
|
29
|
+
*/
|
|
30
|
+
const findSourceBlocks = (tr, isSourceBlock, syncBlockIds) => {
|
|
31
|
+
const matches = [];
|
|
32
|
+
tr.doc.descendants((node, pos) => {
|
|
33
|
+
if (!isSourceBlock(node)) {
|
|
34
|
+
// bodiedSyncBlock is always a top-level node, so there is no need to
|
|
35
|
+
// descend into other branches looking for one.
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (syncBlockIds.some(wanted => matchesRequestedBlock(node, wanted))) {
|
|
39
|
+
matches.push({
|
|
40
|
+
node,
|
|
41
|
+
pos
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Never recurse into a source block's body.
|
|
46
|
+
return false;
|
|
47
|
+
});
|
|
48
|
+
return matches;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Recompute a source `bodiedSyncBlock` delete from the live document.
|
|
53
|
+
*
|
|
54
|
+
* Instead of replaying a transaction that was stashed when the delete was first
|
|
55
|
+
* triggered (and then manually rebased against every intervening edit — the
|
|
56
|
+
* fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>"
|
|
57
|
+
* and assorted position/open-depth errors), this finds the target node(s) by
|
|
58
|
+
* `localId`/`resourceId` in the current state and issues a fresh
|
|
59
|
+
* `tr.delete(pos, pos + nodeSize)` for each.
|
|
60
|
+
*
|
|
61
|
+
* Deletes are applied in reverse document order so earlier deletes do not
|
|
62
|
+
* invalidate the positions of later ones.
|
|
63
|
+
*
|
|
64
|
+
* Partial matches are handled gracefully: when only some of the requested
|
|
65
|
+
* blocks are still present (e.g. a remote collaborator removed the rest while
|
|
66
|
+
* the confirmation modal was open), the found ones are deleted and the missing
|
|
67
|
+
* ones are skipped. This is intentional — the backend deletion for every
|
|
68
|
+
* requested block has already been issued by the store manager, so the local
|
|
69
|
+
* transaction only needs to remove whatever is still in the live document.
|
|
70
|
+
*
|
|
71
|
+
* @returns the mutated transaction when at least one target node was found and
|
|
72
|
+
* deleted (this may be a partial delete if some targets were already gone),
|
|
73
|
+
* otherwise `undefined` when none of the targets exist any more — e.g. a remote
|
|
74
|
+
* collaborator already removed them all — so there is nothing to delete.
|
|
75
|
+
*/
|
|
76
|
+
export const recomputeDeleteTransaction = (tr, isSourceBlock, syncBlockIds) => {
|
|
77
|
+
const matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
|
|
78
|
+
if (matches.length === 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Delete in reverse document order so positions remain valid across deletes.
|
|
83
|
+
matches.sort((a, b) => b.pos - a.pos).forEach(({
|
|
84
|
+
node,
|
|
85
|
+
pos
|
|
86
|
+
}) => {
|
|
87
|
+
tr.delete(pos, pos + node.nodeSize);
|
|
88
|
+
});
|
|
89
|
+
return tr;
|
|
90
|
+
};
|
|
@@ -8,7 +8,7 @@ import { DOMSerializer, Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
|
8
8
|
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
9
9
|
import { findSelectedNodeOfType, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
|
|
10
10
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
11
|
-
import { syncedBlockPluginKey } from '../pm-plugins/main';
|
|
11
|
+
import { deleteMechanismMetaKey, syncedBlockPluginKey } from '../pm-plugins/main';
|
|
12
12
|
import { canBeConvertedToSyncBlock, deferDispatch, findSyncBlock, findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils';
|
|
13
13
|
import { FLAG_ID } from '../types';
|
|
14
14
|
import { findBodiedSyncBlockByLocalId, pasteSyncBlockHTMLContent } from './utils';
|
|
@@ -151,6 +151,10 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
|
|
|
151
151
|
}
|
|
152
152
|
var domNode = toDOM(referenceSyncBlockNode, schema);
|
|
153
153
|
copyDomNode(domNode, referenceSyncBlockNode.type, selection);
|
|
154
|
+
|
|
155
|
+
// Bare-uuid join key shared with the create/delete events: for a source
|
|
156
|
+
// bodiedSyncBlock its `localId` is the source uuid (copy was page-form only).
|
|
157
|
+
var sourceJoinKey = isBodiedSyncBlock ? syncBlockFindResult.node.attrs.localId : undefined;
|
|
154
158
|
deferDispatch(function () {
|
|
155
159
|
api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
|
|
156
160
|
var _api$analytics4;
|
|
@@ -160,10 +164,12 @@ var copySyncedBlockReferenceToClipboardInternal = function copySyncedBlockRefere
|
|
|
160
164
|
action: ACTION.COPIED,
|
|
161
165
|
actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
|
|
162
166
|
actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_COPY,
|
|
163
|
-
attributes: {
|
|
167
|
+
attributes: _objectSpread({
|
|
164
168
|
resourceId: referenceSyncBlockNode.attrs.resourceId,
|
|
165
169
|
inputMethod: inputMethod
|
|
166
|
-
}
|
|
170
|
+
}, sourceJoinKey && fg('platform_editor_blocks_patch_4') && {
|
|
171
|
+
blockInstanceId: sourceJoinKey
|
|
172
|
+
})
|
|
167
173
|
});
|
|
168
174
|
return tr.setMeta(syncedBlockPluginKey, {
|
|
169
175
|
activeFlag: {
|
|
@@ -239,6 +245,13 @@ export var removeSyncedBlock = function removeSyncedBlock(api) {
|
|
|
239
245
|
if (!removeTr) {
|
|
240
246
|
return false;
|
|
241
247
|
}
|
|
248
|
+
|
|
249
|
+
// Tag the transaction so analytics can report this as `deleteButton` rather
|
|
250
|
+
// than a keyboard delete (both produce a plain ReplaceStep). Gated so the
|
|
251
|
+
// gate-off transaction is unchanged.
|
|
252
|
+
if (fg('platform_editor_blocks_patch_4')) {
|
|
253
|
+
removeTr.setMeta(deleteMechanismMetaKey, 'deleteButton');
|
|
254
|
+
}
|
|
242
255
|
dispatch(removeTr);
|
|
243
256
|
api === null || api === void 0 || api.core.actions.focus();
|
|
244
257
|
return true;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
2
2
|
import _createClass from "@babel/runtime/helpers/createClass";
|
|
3
3
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
4
|
+
import _typeof from "@babel/runtime/helpers/typeof";
|
|
4
5
|
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
5
6
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
6
7
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
@@ -14,7 +15,7 @@ import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
|
|
|
14
15
|
import { BodiedSyncBlockSharedCssClassName, SyncBlockStateCssClassName } from '@atlaskit/editor-common/sync-block';
|
|
15
16
|
import { mapSlice, pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
|
|
16
17
|
import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
|
|
17
|
-
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
18
|
+
import { NodeSelection, PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
18
19
|
import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
|
|
19
20
|
import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
|
|
20
21
|
import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
|
|
@@ -34,6 +35,17 @@ import { hasEditInSyncBlock, trackSyncBlocks } from './utils/track-sync-blocks';
|
|
|
34
35
|
import { transactionInsertsSyncedBlock } from './utils/transaction-inserts-synced-block';
|
|
35
36
|
import { deferDispatch, wasExtensionInsertedInBodiedSyncBlock, sliceFullyContainsNode } from './utils/utils';
|
|
36
37
|
export var syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dedicated transaction-meta {@link PluginKey} the synced-block removal command
|
|
41
|
+
* sets so {@link getDeleteMechanism} can report a toolbar Delete as
|
|
42
|
+
* `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
|
|
43
|
+
* plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
|
|
44
|
+
* carries plugin-state transitions read in `apply()`, or a bare string) keeps
|
|
45
|
+
* this transient signal uniquely namespaced and isolated. Only read behind
|
|
46
|
+
* `platform_editor_blocks_patch_4`.
|
|
47
|
+
*/
|
|
48
|
+
export var deleteMechanismMetaKey = new PluginKey('syncedBlockDeleteMechanism');
|
|
37
49
|
var mapRetryCreationPosMap = function mapRetryCreationPosMap(oldMap, newRetryCreationPos, mapPos, isPerfExperimentOn) {
|
|
38
50
|
var resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;
|
|
39
51
|
|
|
@@ -114,6 +126,55 @@ var getDeleteReason = function getDeleteReason(tr) {
|
|
|
114
126
|
}
|
|
115
127
|
return reason;
|
|
116
128
|
};
|
|
129
|
+
|
|
130
|
+
/** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
|
|
131
|
+
var isHistoryMeta = function isHistoryMeta(meta) {
|
|
132
|
+
return _typeof(meta) === 'object' && meta !== null && 'redo' in meta;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
|
|
137
|
+
* analytics dimension. Each value names the user action; most-specific wins:
|
|
138
|
+
* - `undo` / `redo` — history transaction.
|
|
139
|
+
* - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
|
|
140
|
+
* (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
|
|
141
|
+
* - `selectionReplaced` — ReplaceStep on a node-selected block (the
|
|
142
|
+
* accidental-overwrite path: block selected, then typed/pasted over).
|
|
143
|
+
* - `keyboardDelete` — ReplaceStep removal at a caret/range selection
|
|
144
|
+
* (Backspace/Delete key).
|
|
145
|
+
* - `other` — anything else (unsync, conversion, code-dispatched, or a
|
|
146
|
+
* structural `ReplaceAroundStep` such as wrap/lift/unwrap).
|
|
147
|
+
*
|
|
148
|
+
* Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
|
|
149
|
+
* is used for wrapping/lifting/unwrapping content, not direct removal, so a
|
|
150
|
+
* removal carried by one is classified as `other` rather than misreported as a
|
|
151
|
+
* keyboard delete.
|
|
152
|
+
*
|
|
153
|
+
* `state` is the pre-transaction state, so its selection reflects what was
|
|
154
|
+
* selected when the edit was made.
|
|
155
|
+
*/
|
|
156
|
+
export var getDeleteMechanism = function getDeleteMechanism(tr, state) {
|
|
157
|
+
var _selection$node;
|
|
158
|
+
var historyMeta = tr.getMeta(pmHistoryPluginKey);
|
|
159
|
+
if (historyMeta) {
|
|
160
|
+
return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The toolbar Delete control tags its transaction so we can report it
|
|
164
|
+
// distinctly from a keyboard delete (both produce a plain ReplaceStep).
|
|
165
|
+
if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
|
|
166
|
+
return 'deleteButton';
|
|
167
|
+
}
|
|
168
|
+
var hasReplaceStep = tr.steps.some(function (step) {
|
|
169
|
+
return step instanceof ReplaceStep;
|
|
170
|
+
});
|
|
171
|
+
if (!hasReplaceStep) {
|
|
172
|
+
return 'other';
|
|
173
|
+
}
|
|
174
|
+
var selection = state.selection;
|
|
175
|
+
var isNodeSelected = selection instanceof NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock';
|
|
176
|
+
return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
|
|
177
|
+
};
|
|
117
178
|
var filterTransactionOnline = function filterTransactionOnline(_ref3) {
|
|
118
179
|
var _tr$getMeta;
|
|
119
180
|
var tr = _ref3.tr,
|
|
@@ -164,9 +225,18 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
|
|
|
164
225
|
});
|
|
165
226
|
});
|
|
166
227
|
if (bodiedSyncBlockRemoved.length > 0) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
228
|
+
if (!fg('platform_editor_blocks_patch_4')) {
|
|
229
|
+
// Legacy stash-and-rebase path: store the original delete transaction so it
|
|
230
|
+
// can be rebased against intervening edits and replayed on confirm. When
|
|
231
|
+
// platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
|
|
232
|
+
// from the live document on confirm (see handleBodiedSyncBlockRemoval), so
|
|
233
|
+
// there is no need to stash the transaction here. See EDITOR-7889.
|
|
234
|
+
// eslint-disable-next-line no-param-reassign
|
|
235
|
+
confirmationTransactionRef.current = tr;
|
|
236
|
+
}
|
|
237
|
+
// Only derive the mechanism behind the gate — discarded gate-off.
|
|
238
|
+
var mechanism = fg('platform_editor_blocks_patch_4') ? getDeleteMechanism(tr, state) : undefined;
|
|
239
|
+
return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism);
|
|
170
240
|
}
|
|
171
241
|
if (bodiedSyncBlockAdded.length > 0) {
|
|
172
242
|
if (tr.getMeta(pmHistoryPluginKey)) {
|
|
@@ -880,13 +950,19 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
|
|
|
880
950
|
}
|
|
881
951
|
});
|
|
882
952
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
953
|
+
|
|
954
|
+
// Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
|
|
955
|
+
// delete is recomputed fresh from the live document on confirm, so there is
|
|
956
|
+
// no stashed transaction to rebase here. See EDITOR-7889.
|
|
957
|
+
if (!fg('platform_editor_blocks_patch_4')) {
|
|
958
|
+
trs.filter(function (tr) {
|
|
959
|
+
return tr.docChanged;
|
|
960
|
+
}).forEach(function (tr) {
|
|
961
|
+
if (confirmationTransactionRef.current) {
|
|
962
|
+
confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
}
|
|
890
966
|
var _iterator2 = _createForOfIteratorHelper(trs),
|
|
891
967
|
_step2;
|
|
892
968
|
try {
|
|
@@ -2,8 +2,10 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
|
2
2
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
3
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
4
|
import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
|
|
5
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
5
6
|
import { FLAG_ID } from '../../types';
|
|
6
7
|
import { syncedBlockPluginKey } from '../main';
|
|
8
|
+
import { recomputeDeleteTransaction } from './recompute-delete-transaction';
|
|
7
9
|
var onRetry = function onRetry(api, syncBlockStore) {
|
|
8
10
|
return function () {
|
|
9
11
|
var _api$core;
|
|
@@ -25,7 +27,7 @@ var onDismissed = function onDismissed(syncBlockStore) {
|
|
|
25
27
|
});
|
|
26
28
|
};
|
|
27
29
|
};
|
|
28
|
-
export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason) {
|
|
30
|
+
export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, deletionReason, mechanism) {
|
|
29
31
|
// Clear potential old pending deletion to retreat the deletion as first attempt
|
|
30
32
|
syncBlockStore.sourceManager.clearPendingDeletion();
|
|
31
33
|
|
|
@@ -36,12 +38,45 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
|
|
|
36
38
|
syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(bodiedSyncBlockRemoved.map(function (node) {
|
|
37
39
|
return node.attrs;
|
|
38
40
|
}), deletionReason, function () {
|
|
39
|
-
var _api$
|
|
41
|
+
var _api$core3;
|
|
42
|
+
if (fg('platform_editor_blocks_patch_4')) {
|
|
43
|
+
var _api$core2;
|
|
44
|
+
// Recompute the delete fresh from the live document instead of
|
|
45
|
+
// replaying a stashed-and-rebased transaction. The stash/rebase
|
|
46
|
+
// approach produced stale, schema-invalid transactions when the
|
|
47
|
+
// document changed shape (local edits or remote collab) while the
|
|
48
|
+
// confirmation modal was open — the dominant signature being
|
|
49
|
+
// "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889.
|
|
50
|
+
api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref2) {
|
|
51
|
+
var tr = _ref2.tr;
|
|
52
|
+
var recomputedTr = recomputeDeleteTransaction(tr, syncBlockStore.sourceManager.isSourceBlock, bodiedSyncBlockRemoved.map(function (node) {
|
|
53
|
+
return node.attrs;
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// The target node(s) no longer exist in the live document (e.g.
|
|
57
|
+
// a remote collaborator already removed them). There is nothing
|
|
58
|
+
// to delete locally, so return `null` to skip the dispatch
|
|
59
|
+
// entirely — dispatching the untouched transaction would be a
|
|
60
|
+
// no-op that still runs the plugin's filter/append hooks. The
|
|
61
|
+
// backend deletion has already been issued by the store manager.
|
|
62
|
+
if (!recomputedTr) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true);
|
|
66
|
+
if (!recomputedTr.getMeta(pmHistoryPluginKey)) {
|
|
67
|
+
// bodiedSyncBlock deletion is expected to be permanent (cannot undo)
|
|
68
|
+
// For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone
|
|
69
|
+
recomputedTr.setMeta('addToHistory', false);
|
|
70
|
+
}
|
|
71
|
+
return recomputedTr;
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
40
75
|
var confirmationTransaction = confirmationTransactionRef.current;
|
|
41
76
|
if (!confirmationTransaction) {
|
|
42
77
|
return;
|
|
43
78
|
}
|
|
44
|
-
api === null || api === void 0 || (_api$
|
|
79
|
+
api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function () {
|
|
45
80
|
var trToDispatch = confirmationTransaction.setMeta('isConfirmedSyncBlockDeletion', true);
|
|
46
81
|
if (!trToDispatch.getMeta(pmHistoryPluginKey)) {
|
|
47
82
|
// bodiedSyncBlock deletion is expected to be permanent (cannot undo)
|
|
@@ -51,9 +86,9 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
|
|
|
51
86
|
return trToDispatch;
|
|
52
87
|
});
|
|
53
88
|
}, function (success) {
|
|
54
|
-
var _api$
|
|
55
|
-
api === null || api === void 0 || (_api$
|
|
56
|
-
var tr =
|
|
89
|
+
var _api$core4;
|
|
90
|
+
api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
|
|
91
|
+
var tr = _ref3.tr;
|
|
57
92
|
var newState;
|
|
58
93
|
if (!success) {
|
|
59
94
|
newState = {
|
|
@@ -79,6 +114,6 @@ export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(
|
|
|
79
114
|
});
|
|
80
115
|
}, function () {
|
|
81
116
|
confirmationTransactionRef.current = undefined;
|
|
82
|
-
});
|
|
117
|
+
}, mechanism);
|
|
83
118
|
return false;
|
|
84
119
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `node` is the source block requested by `wanted`.
|
|
3
|
+
*
|
|
4
|
+
* Matching is done *per requested block* (rather than against global pools of
|
|
5
|
+
* every requested localId/resourceId) so a node cannot be matched by combining
|
|
6
|
+
* one block's `localId` with a different block's `resourceId`. We match on the
|
|
7
|
+
* requested block's `localId` first and only fall back to its `resourceId` when
|
|
8
|
+
* the `localId` has been regenerated (so it no longer matches the live node).
|
|
9
|
+
*/
|
|
10
|
+
var matchesRequestedBlock = function matchesRequestedBlock(node, wanted) {
|
|
11
|
+
var _ref = node.attrs,
|
|
12
|
+
localId = _ref.localId,
|
|
13
|
+
resourceId = _ref.resourceId;
|
|
14
|
+
if (wanted.localId && localId && wanted.localId === localId) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return Boolean(wanted.resourceId && resourceId && wanted.resourceId === resourceId);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Locate the live positions of the source `bodiedSyncBlock` nodes identified by
|
|
22
|
+
* `syncBlockIds`, matching first on `localId` and falling back to `resourceId`.
|
|
23
|
+
*
|
|
24
|
+
* This walks the *current* document (`tr.doc`) rather than relying on positions
|
|
25
|
+
* captured when the delete was first triggered, so it is robust to intervening
|
|
26
|
+
* local edits and remote collab changes that occur while the confirmation modal
|
|
27
|
+
* is open.
|
|
28
|
+
*/
|
|
29
|
+
var findSourceBlocks = function findSourceBlocks(tr, isSourceBlock, syncBlockIds) {
|
|
30
|
+
var matches = [];
|
|
31
|
+
tr.doc.descendants(function (node, pos) {
|
|
32
|
+
if (!isSourceBlock(node)) {
|
|
33
|
+
// bodiedSyncBlock is always a top-level node, so there is no need to
|
|
34
|
+
// descend into other branches looking for one.
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (syncBlockIds.some(function (wanted) {
|
|
38
|
+
return matchesRequestedBlock(node, wanted);
|
|
39
|
+
})) {
|
|
40
|
+
matches.push({
|
|
41
|
+
node: node,
|
|
42
|
+
pos: pos
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Never recurse into a source block's body.
|
|
47
|
+
return false;
|
|
48
|
+
});
|
|
49
|
+
return matches;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recompute a source `bodiedSyncBlock` delete from the live document.
|
|
54
|
+
*
|
|
55
|
+
* Instead of replaying a transaction that was stashed when the delete was first
|
|
56
|
+
* triggered (and then manually rebased against every intervening edit — the
|
|
57
|
+
* fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>"
|
|
58
|
+
* and assorted position/open-depth errors), this finds the target node(s) by
|
|
59
|
+
* `localId`/`resourceId` in the current state and issues a fresh
|
|
60
|
+
* `tr.delete(pos, pos + nodeSize)` for each.
|
|
61
|
+
*
|
|
62
|
+
* Deletes are applied in reverse document order so earlier deletes do not
|
|
63
|
+
* invalidate the positions of later ones.
|
|
64
|
+
*
|
|
65
|
+
* Partial matches are handled gracefully: when only some of the requested
|
|
66
|
+
* blocks are still present (e.g. a remote collaborator removed the rest while
|
|
67
|
+
* the confirmation modal was open), the found ones are deleted and the missing
|
|
68
|
+
* ones are skipped. This is intentional — the backend deletion for every
|
|
69
|
+
* requested block has already been issued by the store manager, so the local
|
|
70
|
+
* transaction only needs to remove whatever is still in the live document.
|
|
71
|
+
*
|
|
72
|
+
* @returns the mutated transaction when at least one target node was found and
|
|
73
|
+
* deleted (this may be a partial delete if some targets were already gone),
|
|
74
|
+
* otherwise `undefined` when none of the targets exist any more — e.g. a remote
|
|
75
|
+
* collaborator already removed them all — so there is nothing to delete.
|
|
76
|
+
*/
|
|
77
|
+
export var recomputeDeleteTransaction = function recomputeDeleteTransaction(tr, isSourceBlock, syncBlockIds) {
|
|
78
|
+
var matches = findSourceBlocks(tr, isSourceBlock, syncBlockIds);
|
|
79
|
+
if (matches.length === 0) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Delete in reverse document order so positions remain valid across deletes.
|
|
84
|
+
matches.sort(function (a, b) {
|
|
85
|
+
return b.pos - a.pos;
|
|
86
|
+
}).forEach(function (_ref2) {
|
|
87
|
+
var node = _ref2.node,
|
|
88
|
+
pos = _ref2.pos;
|
|
89
|
+
tr.delete(pos, pos + node.nodeSize);
|
|
90
|
+
});
|
|
91
|
+
return tr;
|
|
92
|
+
};
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
2
|
import type { ExtractInjectionAPI, PMPluginFactoryParams } from '@atlaskit/editor-common/types';
|
|
3
|
+
import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
|
|
3
4
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
4
5
|
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
5
6
|
import type { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
|
|
7
|
+
import type { DeletionMechanism } from '@atlaskit/editor-synced-block-provider/common/types';
|
|
6
8
|
import type { SyncedBlockPlugin, SyncedBlockPluginOptions } from '../syncedBlockPluginType';
|
|
7
9
|
import type { ActiveFlag, BodiedSyncBlockDeletionStatus, RetryCreationPosMap } from '../types';
|
|
8
10
|
export declare const syncedBlockPluginKey: PluginKey;
|
|
11
|
+
/**
|
|
12
|
+
* Dedicated transaction-meta {@link PluginKey} the synced-block removal command
|
|
13
|
+
* sets so {@link getDeleteMechanism} can report a toolbar Delete as
|
|
14
|
+
* `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
|
|
15
|
+
* plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
|
|
16
|
+
* carries plugin-state transitions read in `apply()`, or a bare string) keeps
|
|
17
|
+
* this transient signal uniquely namespaced and isolated. Only read behind
|
|
18
|
+
* `platform_editor_blocks_patch_4`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const deleteMechanismMetaKey: PluginKey<DeletionMechanism>;
|
|
9
21
|
type SyncedBlockPluginState = {
|
|
10
22
|
activeFlag: ActiveFlag;
|
|
11
23
|
bodiedSyncBlockDeletionStatus?: BodiedSyncBlockDeletionStatus;
|
|
@@ -42,5 +54,27 @@ type SyncedBlockPluginState = {
|
|
|
42
54
|
statusDecorationSet: DecorationSet;
|
|
43
55
|
syncBlockStore: SyncBlockStoreManager;
|
|
44
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
|
|
59
|
+
* analytics dimension. Each value names the user action; most-specific wins:
|
|
60
|
+
* - `undo` / `redo` — history transaction.
|
|
61
|
+
* - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
|
|
62
|
+
* (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
|
|
63
|
+
* - `selectionReplaced` — ReplaceStep on a node-selected block (the
|
|
64
|
+
* accidental-overwrite path: block selected, then typed/pasted over).
|
|
65
|
+
* - `keyboardDelete` — ReplaceStep removal at a caret/range selection
|
|
66
|
+
* (Backspace/Delete key).
|
|
67
|
+
* - `other` — anything else (unsync, conversion, code-dispatched, or a
|
|
68
|
+
* structural `ReplaceAroundStep` such as wrap/lift/unwrap).
|
|
69
|
+
*
|
|
70
|
+
* Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
|
|
71
|
+
* is used for wrapping/lifting/unwrapping content, not direct removal, so a
|
|
72
|
+
* removal carried by one is classified as `other` rather than misreported as a
|
|
73
|
+
* keyboard delete.
|
|
74
|
+
*
|
|
75
|
+
* `state` is the pre-transaction state, so its selection reflects what was
|
|
76
|
+
* selected when the edit was made.
|
|
77
|
+
*/
|
|
78
|
+
export declare const getDeleteMechanism: (tr: Transaction, state: EditorState) => DeletionMechanism;
|
|
45
79
|
export declare const createPlugin: (options: SyncedBlockPluginOptions | undefined, pmPluginFactoryParams: PMPluginFactoryParams, syncBlockStore: SyncBlockStoreManager, api?: ExtractInjectionAPI<SyncedBlockPlugin>) => SafePlugin<SyncedBlockPluginState>;
|
|
46
80
|
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
|
|
2
2
|
import type { Transaction } from '@atlaskit/editor-prosemirror/state';
|
|
3
|
-
import type {
|
|
3
|
+
import type { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
|
|
4
|
+
import type { DeletionMechanism, DeletionReason } from '@atlaskit/editor-synced-block-provider/common/types';
|
|
4
5
|
import type { SyncedBlockPlugin } from '../../syncedBlockPluginType';
|
|
5
6
|
import type { SyncBlockInfo } from '../../types';
|
|
6
7
|
export type TransactionRef = {
|
|
7
8
|
current: Transaction | undefined;
|
|
8
9
|
};
|
|
9
|
-
export declare const handleBodiedSyncBlockRemoval: (bodiedSyncBlockRemoved: SyncBlockInfo[], syncBlockStore: SyncBlockStoreManager, api: ExtractInjectionAPI<SyncedBlockPlugin> | undefined, confirmationTransactionRef: TransactionRef, deletionReason: DeletionReason) => boolean;
|
|
10
|
+
export declare const handleBodiedSyncBlockRemoval: (bodiedSyncBlockRemoved: SyncBlockInfo[], syncBlockStore: SyncBlockStoreManager, api: ExtractInjectionAPI<SyncedBlockPlugin> | undefined, confirmationTransactionRef: TransactionRef, deletionReason: DeletionReason, mechanism?: DeletionMechanism) => boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
|
|
2
|
+
import type { Transaction } from '@atlaskit/editor-prosemirror/state';
|
|
3
|
+
import type { SyncBlockAttrs } from '../../types';
|
|
4
|
+
/**
|
|
5
|
+
* Recompute a source `bodiedSyncBlock` delete from the live document.
|
|
6
|
+
*
|
|
7
|
+
* Instead of replaying a transaction that was stashed when the delete was first
|
|
8
|
+
* triggered (and then manually rebased against every intervening edit — the
|
|
9
|
+
* fragile pattern that produced "Invalid content for node bodiedSyncBlock: <>"
|
|
10
|
+
* and assorted position/open-depth errors), this finds the target node(s) by
|
|
11
|
+
* `localId`/`resourceId` in the current state and issues a fresh
|
|
12
|
+
* `tr.delete(pos, pos + nodeSize)` for each.
|
|
13
|
+
*
|
|
14
|
+
* Deletes are applied in reverse document order so earlier deletes do not
|
|
15
|
+
* invalidate the positions of later ones.
|
|
16
|
+
*
|
|
17
|
+
* Partial matches are handled gracefully: when only some of the requested
|
|
18
|
+
* blocks are still present (e.g. a remote collaborator removed the rest while
|
|
19
|
+
* the confirmation modal was open), the found ones are deleted and the missing
|
|
20
|
+
* ones are skipped. This is intentional — the backend deletion for every
|
|
21
|
+
* requested block has already been issued by the store manager, so the local
|
|
22
|
+
* transaction only needs to remove whatever is still in the live document.
|
|
23
|
+
*
|
|
24
|
+
* @returns the mutated transaction when at least one target node was found and
|
|
25
|
+
* deleted (this may be a partial delete if some targets were already gone),
|
|
26
|
+
* otherwise `undefined` when none of the targets exist any more — e.g. a remote
|
|
27
|
+
* collaborator already removed them all — so there is nothing to delete.
|
|
28
|
+
*/
|
|
29
|
+
export declare const recomputeDeleteTransaction: (tr: Transaction, isSourceBlock: (node: PMNode) => boolean, syncBlockIds: SyncBlockAttrs[]) => Transaction | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-synced-block",
|
|
3
|
-
"version": "10.0
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"description": "SyncedBlock plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@atlaskit/editor-plugin-user-intent": "^10.0.0",
|
|
36
36
|
"@atlaskit/editor-prosemirror": "^8.0.0",
|
|
37
37
|
"@atlaskit/editor-shared-styles": "^4.0.0",
|
|
38
|
-
"@atlaskit/editor-synced-block-provider": "^8.
|
|
38
|
+
"@atlaskit/editor-synced-block-provider": "^8.5.0",
|
|
39
39
|
"@atlaskit/editor-toolbar": "^2.1.0",
|
|
40
40
|
"@atlaskit/flag": "^18.1.0",
|
|
41
41
|
"@atlaskit/icon": "36.1.0",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"date-fns": "^2.17.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@atlaskit/editor-common": "^116.
|
|
59
|
+
"@atlaskit/editor-common": "^116.19.0",
|
|
60
60
|
"react": "^18.2.0",
|
|
61
61
|
"react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
|
|
62
62
|
},
|
|
@@ -108,6 +108,9 @@
|
|
|
108
108
|
"platform_editor_blocks_patch_2": {
|
|
109
109
|
"type": "boolean"
|
|
110
110
|
},
|
|
111
|
+
"platform_editor_blocks_patch_4": {
|
|
112
|
+
"type": "boolean"
|
|
113
|
+
},
|
|
111
114
|
"platform_synced_block_patch_13": {
|
|
112
115
|
"type": "boolean"
|
|
113
116
|
},
|