@atlaskit/editor-plugin-synced-block 10.0.20 → 10.0.21
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 +10 -0
- package/dist/cjs/pm-plugins/main.js +22 -9
- package/dist/cjs/pm-plugins/utils/handle-bodied-sync-block-removal.js +40 -5
- package/dist/cjs/pm-plugins/utils/recompute-delete-transaction.js +98 -0
- package/dist/es2019/pm-plugins/main.js +20 -7
- package/dist/es2019/pm-plugins/utils/handle-bodied-sync-block-removal.js +38 -4
- package/dist/es2019/pm-plugins/utils/recompute-delete-transaction.js +90 -0
- package/dist/esm/pm-plugins/main.js +22 -9
- package/dist/esm/pm-plugins/utils/handle-bodied-sync-block-removal.js +40 -5
- package/dist/esm/pm-plugins/utils/recompute-delete-transaction.js +92 -0
- package/dist/types/pm-plugins/utils/recompute-delete-transaction.d.ts +29 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-synced-block
|
|
2
2
|
|
|
3
|
+
## 10.0.21
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`845de90a5f39e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/845de90a5f39e) -
|
|
8
|
+
[ux] Fix source synced block deletion intermittently failing when the document changes (from your
|
|
9
|
+
own edits or a collaborator's) while the delete confirmation dialog is open. The delete is now
|
|
10
|
+
recomputed from the current document on confirm, instead of replaying a stale transaction captured
|
|
11
|
+
when the dialog opened. Gated behind `platform_editor_blocks_patch_4`.
|
|
12
|
+
|
|
3
13
|
## 10.0.20
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
|
@@ -171,8 +171,15 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
|
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
if (bodiedSyncBlockRemoved.length > 0) {
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
if (!(0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
|
|
175
|
+
// Legacy stash-and-rebase path: store the original delete transaction so it
|
|
176
|
+
// can be rebased against intervening edits and replayed on confirm. When
|
|
177
|
+
// platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
|
|
178
|
+
// from the live document on confirm (see handleBodiedSyncBlockRemoval), so
|
|
179
|
+
// there is no need to stash the transaction here. See EDITOR-7889.
|
|
180
|
+
// eslint-disable-next-line no-param-reassign
|
|
181
|
+
confirmationTransactionRef.current = tr;
|
|
182
|
+
}
|
|
176
183
|
return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
|
|
177
184
|
}
|
|
178
185
|
if (bodiedSyncBlockAdded.length > 0) {
|
|
@@ -887,13 +894,19 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
|
|
|
887
894
|
}
|
|
888
895
|
});
|
|
889
896
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
+
|
|
898
|
+
// Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
|
|
899
|
+
// delete is recomputed fresh from the live document on confirm, so there is
|
|
900
|
+
// no stashed transaction to rebase here. See EDITOR-7889.
|
|
901
|
+
if (!(0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_4')) {
|
|
902
|
+
trs.filter(function (tr) {
|
|
903
|
+
return tr.docChanged;
|
|
904
|
+
}).forEach(function (tr) {
|
|
905
|
+
if (confirmationTransactionRef.current) {
|
|
906
|
+
confirmationTransactionRef.current = (0, _editorSyncedBlockProvider.rebaseTransaction)(confirmationTransactionRef.current, tr, newState);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
}
|
|
897
910
|
var _iterator2 = _createForOfIteratorHelper(trs),
|
|
898
911
|
_step2;
|
|
899
912
|
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) {
|
|
@@ -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 = {
|
|
@@ -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
|
+
};
|
|
@@ -147,8 +147,15 @@ const filterTransactionOnline = ({
|
|
|
147
147
|
});
|
|
148
148
|
});
|
|
149
149
|
if (bodiedSyncBlockRemoved.length > 0) {
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
if (!fg('platform_editor_blocks_patch_4')) {
|
|
151
|
+
// Legacy stash-and-rebase path: store the original delete transaction so it
|
|
152
|
+
// can be rebased against intervening edits and replayed on confirm. When
|
|
153
|
+
// platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
|
|
154
|
+
// from the live document on confirm (see handleBodiedSyncBlockRemoval), so
|
|
155
|
+
// there is no need to stash the transaction here. See EDITOR-7889.
|
|
156
|
+
// eslint-disable-next-line no-param-reassign
|
|
157
|
+
confirmationTransactionRef.current = tr;
|
|
158
|
+
}
|
|
152
159
|
return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
|
|
153
160
|
}
|
|
154
161
|
if (bodiedSyncBlockAdded.length > 0) {
|
|
@@ -844,11 +851,17 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
844
851
|
}
|
|
845
852
|
});
|
|
846
853
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
854
|
+
|
|
855
|
+
// Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
|
|
856
|
+
// delete is recomputed fresh from the live document on confirm, so there is
|
|
857
|
+
// no stashed transaction to rebase here. See EDITOR-7889.
|
|
858
|
+
if (!fg('platform_editor_blocks_patch_4')) {
|
|
859
|
+
trs.filter(tr => tr.docChanged).forEach(tr => {
|
|
860
|
+
if (confirmationTransactionRef.current) {
|
|
861
|
+
confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
}
|
|
852
865
|
for (const tr of trs) {
|
|
853
866
|
if (tr.getMeta(pmHistoryPluginKey)) {
|
|
854
867
|
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(({
|
|
@@ -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;
|
|
@@ -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
|
+
};
|
|
@@ -164,8 +164,15 @@ var filterTransactionOnline = function filterTransactionOnline(_ref3) {
|
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
166
|
if (bodiedSyncBlockRemoved.length > 0) {
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
if (!fg('platform_editor_blocks_patch_4')) {
|
|
168
|
+
// Legacy stash-and-rebase path: store the original delete transaction so it
|
|
169
|
+
// can be rebased against intervening edits and replayed on confirm. When
|
|
170
|
+
// platform_editor_blocks_patch_4 is on we instead recompute the delete fresh
|
|
171
|
+
// from the live document on confirm (see handleBodiedSyncBlockRemoval), so
|
|
172
|
+
// there is no need to stash the transaction here. See EDITOR-7889.
|
|
173
|
+
// eslint-disable-next-line no-param-reassign
|
|
174
|
+
confirmationTransactionRef.current = tr;
|
|
175
|
+
}
|
|
169
176
|
return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
|
|
170
177
|
}
|
|
171
178
|
if (bodiedSyncBlockAdded.length > 0) {
|
|
@@ -880,13 +887,19 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
|
|
|
880
887
|
}
|
|
881
888
|
});
|
|
882
889
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
+
|
|
891
|
+
// Legacy stash-and-rebase path. When platform_editor_blocks_patch_4 is on the
|
|
892
|
+
// delete is recomputed fresh from the live document on confirm, so there is
|
|
893
|
+
// no stashed transaction to rebase here. See EDITOR-7889.
|
|
894
|
+
if (!fg('platform_editor_blocks_patch_4')) {
|
|
895
|
+
trs.filter(function (tr) {
|
|
896
|
+
return tr.docChanged;
|
|
897
|
+
}).forEach(function (tr) {
|
|
898
|
+
if (confirmationTransactionRef.current) {
|
|
899
|
+
confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
890
903
|
var _iterator2 = _createForOfIteratorHelper(trs),
|
|
891
904
|
_step2;
|
|
892
905
|
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;
|
|
@@ -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 = {
|
|
@@ -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
|
+
};
|
|
@@ -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.0.21",
|
|
4
4
|
"description": "SyncedBlock plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -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
|
},
|