@atlaskit/editor-plugin-synced-block 6.0.44 → 6.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,133 +1,54 @@
1
- # Synced Blocks — Developer Agent Guide
1
+ # Synced Blocks Plugin — Developer Agent Guide
2
2
 
3
- > **Purpose**: This guide helps AI agents and developers implement features, fix bugs, and clean up
4
- > feature gates in the Synced Blocks codebase. It provides architectural context, key file
5
- > locations, common patterns, and debugging guidance.
6
- >
7
- > **Full Knowledge Base**:
8
- > [Synced Blocks — Comprehensive Knowledge Base](https://hello.atlassian.net/wiki/spaces/egcuc/pages/6679548384)
9
- > (Confluence)
3
+ > **For workflow guidance, debugging, and cross-package task guides, load the `synced-blocks` skill:**
4
+ > `get_skill(skill_name_or_path="platform/packages/editor/.rovodev/skills/synced-blocks/SKILL.md")`
10
5
 
11
6
  ---
12
7
 
13
8
  ## Quick Context
14
9
 
15
10
  **Synced Blocks** lets users create reusable content blocks (source) that can be referenced across
16
- Confluence pages and Jira issue descriptions. Edits to the source propagate to all references in
17
- near real-time via the Block Service backend and AGG GraphQL WebSocket subscriptions.
11
+ Confluence pages and Jira issue descriptions. This package is the core editor plugin it registers
12
+ ADF nodes, toolbar/menu integration, commands, and ProseMirror plugins.
18
13
 
19
14
  **Two ADF node types:**
20
15
 
21
16
  - `bodiedSyncBlock` — **Source** sync block (contains the editable content)
22
- - `syncBlock` — **Reference** sync block (renders content fetched from block service)
17
+ - `syncBlock` — **Reference** sync block (renders content fetched from Block Service)
23
18
 
24
19
  ---
25
20
 
26
- ## Package Map
27
-
28
- ### Platform (shared across products)
29
-
30
- | Package | Path | Purpose |
31
- | ------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
32
- | **Plugin** | `platform/packages/editor/editor-plugin-synced-block/` | Core editor plugin — registers nodes, toolbar, commands, block menu integration |
33
- | **Provider** | `platform/packages/editor/editor-synced-block-provider/` | Data layer — store managers, block service API client, ARI generation, permissions, media tokens |
34
- | **Renderer** | `platform/packages/editor/editor-synced-block-renderer/` | View-mode rendering of reference sync blocks using nested renderer |
35
- | **Plugin Tests** | `platform/packages/editor/editor-plugin-synced-block-tests/` | Integration tests for the plugin |
36
- | **Provider Tests** | `platform/packages/editor/editor-synced-block-provider-tests/` | Tests for store managers and provider hooks |
37
- | **Renderer Tests** | `platform/packages/editor/editor-synced-block-renderer-tests/` | Tests for renderer components |
38
-
39
- Also touches:
40
-
41
- - `platform/packages/editor/editor-common` — shared types
42
- - `platform/packages/editor/editor-core` — plugin registration
43
- - `platform/packages/renderer` — reference rendering in view mode
44
-
45
- ### Confluence
46
-
47
- | File/Package | Path | Purpose |
48
- | ------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
49
- | SyncedBlockProvider | `confluence/next/packages/fabric-providers/src/SyncedBlockProvider.ts` | Wraps platform provider with Confluence config (AGG endpoint, media tokens, permissions) |
50
- | SyncedBlockPreload | `confluence/next/packages/fabric-providers/src/SyncedBlockPreload.ts` | Preloads sync block data for SSR |
51
- | SSR Data Loading | `confluence/next/packages/fabric-providers/src/SyncedBlockLoadSSRData.ts` | Loads SSR data |
52
- | Edit Page Preload | `confluence/next/packages/load-edit-page/src/preload/preloadSyncedBlocksData.ts` | Preloads data for edit page |
53
- | Editor Preload | `confluence/next/packages/load-edit-page/src/preload/preloadEditorSyncedBlocksData.ts` | Editor-specific preloading |
54
- | Full Page Editor | `confluence/next/packages/full-page-editor/src/FullPageEditorComponent.tsx` | Integrates sync block plugin into editor |
55
-
56
- ### Jira
57
-
58
- | File/Package | Path | Purpose |
59
- | ------------------- | ----------------------------------------------------------- | --------------------------------------------- |
60
- | Provider Package | `jira/src/packages/issue/issue-view-synced-block-provider/` | Core Jira integration package |
61
- | Node Component Hook | `src/useSyncedBlockProviderNodeComponent.tsx` | Creates synced block node components for Jira |
62
- | Editor HOC | `src/withSyncedBlockProviderEditor.tsx` | Wraps Jira editor with sync block support |
63
- | Renderer HOC | `src/withSyncedBlockProviderRenderer.tsx` | Wraps Jira renderer with sync block support |
64
- | Prefetch | `src/prefetchSyncedBlocks.ts` | Prefetches sync block data in issue view |
65
- | Relay Fetch | `src/useRelaySyncBlockFetchProvider.tsx` | Real-time updates via Relay subscriptions |
66
-
67
- ---
68
-
69
- ## Key Concepts
70
-
71
- ### Source vs Reference
72
-
73
- - **Source** (`bodiedSyncBlock`): The original content block. Content is stored in the page ADF AND
74
- in the Block Service. Editable inline.
75
- - **Reference** (`syncBlock`): A read-only copy that fetches content from Block Service. Rendered
76
- via nested renderer. Shows "Synced from [page]" label.
77
-
78
- ### Data Flow
79
-
80
- 1. **Create source** → Content saved to Block Service via API → ARI generated from page ID + local
81
- ID
82
- 2. **Create reference** → Copy source link → Paste → `syncBlock` node inserted with `resourceId`
83
- 3. **Edit source** → Content pushed to Block Service → AGG subscription notifies references →
84
- References re-fetch and re-render
85
- 4. **SSR** → On page load, Confluence/Jira preloads sync block data from Block Service in bulk
86
-
87
- ### ARI Format
88
-
89
- - Confluence: `ari:cloud:block::{cloudId}/confluence-page:{pageId}/{localId}`
90
- - Jira: `ari:cloud:block::{cloudId}/jira-work-item:{issueId}/{localId}`
91
-
92
- ### Store Managers
93
-
94
- - `SyncBlockStoreManager` — Manages source sync block state (create, update, delete, flush)
95
- - `ReferenceSyncBlockStoreManager` — Manages reference sync block state (fetch, subscribe, cache)
96
- - `SyncBlockInMemorySessionCache` — Caches data for view→edit transitions
97
-
98
- ---
99
-
100
- ## Plugin Internals (`editor-plugin-synced-block/src/`)
101
-
102
- The plugin follows a layered architecture:
21
+ ## Plugin Internals (`src/`)
103
22
 
104
23
  ```
105
- syncedBlockPlugin.tsx ← Top-level: registers nodes, commands, UI, pm-plugins
106
- ├── syncedBlockPluginType.ts TypeScript interfaces for options, shared state, dependencies
24
+ src/
25
+ ├── index.ts # Re-exports plugin + type
26
+ ├── syncedBlockPlugin.tsx # Top-level: registers nodes, commands, UI, pm-plugins
27
+ ├── syncedBlockPluginType.ts # TypeScript interfaces for options, shared state, dependencies
107
28
  ├── editor-commands/
108
- │ └── index.ts createSyncedBlock, copySyncedBlockReferenceToClipboardEditorCommand,
29
+ │ └── index.ts # createSyncedBlock, copySyncedBlockReferenceToClipboardEditorCommand,
109
30
  │ removeSyncedBlockAtPos, unsyncSyncBlock
110
31
  ├── nodeviews/
111
- │ ├── syncedBlock.tsx NodeView for reference (syncBlock) — read-only, fetches from BE
112
- │ ├── bodiedSyncedBlock.tsx NodeView for source (bodiedSyncBlock) — nested editor with content
113
- │ └── bodiedSyncBlockNodeWithToDOMFixed.ts DOM serialization fix variant (experiment-gated)
32
+ │ ├── syncedBlock.tsx # NodeView for reference (syncBlock) — read-only, fetches from BE
33
+ │ ├── bodiedSyncedBlock.tsx # NodeView for source (bodiedSyncBlock) — nested editor with content
34
+ │ └── bodiedSyncBlockNodeWithToDOMFixed.ts # DOM serialization fix variant (experiment-gated)
114
35
  ├── pm-plugins/
115
- │ ├── main.ts Core state machine: sync block lifecycle, creation, deletion, cache
116
- │ ├── menu-and-toolbar-experiences.ts Experience tracking for menu/toolbar interactions
36
+ │ ├── main.ts # Core state machine: sync block lifecycle, creation, deletion, cache
37
+ │ ├── menu-and-toolbar-experiences.ts # Experience tracking for menu/toolbar interactions
117
38
  │ └── utils/
118
- │ ├── track-sync-blocks.ts Tracks mutations, updates shared state
119
- │ ├── handle-bodied-sync-block-creation.ts Creation flow, local cache, retry logic
120
- │ └── handle-bodied-sync-block-removal.ts Deletion flow, BE synchronization
39
+ │ ├── track-sync-blocks.ts # Tracks mutations, updates shared state
40
+ │ ├── handle-bodied-sync-block-creation.ts # Creation flow, local cache, retry logic
41
+ │ └── handle-bodied-sync-block-removal.ts # Deletion flow, BE synchronization
121
42
  ├── ui/
122
- │ ├── toolbar-components.tsx Primary toolbar button ("Create Synced Block")
123
- │ ├── floating-toolbar.tsx Node-level actions: delete, unsync, copy link, view locations
124
- │ ├── block-menu-components.tsx Block menu entry
125
- │ ├── quick-insert.tsx Slash command / quick insert config
126
- │ ├── DeleteConfirmationModal.tsx Deletion confirmation dialog
127
- │ ├── SyncBlockRefresher.tsx Periodic data refresh from backend
128
- │ └── Flag.tsx Error/info flags (offline, copy notifications)
43
+ │ ├── toolbar-components.tsx # Primary toolbar button ("Create Synced Block")
44
+ │ ├── floating-toolbar.tsx # Node-level actions: delete, unsync, copy link, view locations
45
+ │ ├── block-menu-components.tsx # Block menu entry
46
+ │ ├── quick-insert.tsx # Slash command / quick insert config
47
+ │ ├── DeleteConfirmationModal.tsx # Deletion confirmation dialog
48
+ │ ├── SyncBlockRefresher.tsx # Periodic data refresh from backend
49
+ │ └── Flag.tsx # Error/info flags (offline, copy notifications)
129
50
  └── types/
130
- └── index.ts FLAG_ID, SyncedBlockSharedState, BodiedSyncBlockDeletionStatus
51
+ └── index.ts # FLAG_ID, SyncedBlockSharedState, BodiedSyncBlockDeletionStatus
131
52
  ```
132
53
 
133
54
  ### Key Code Patterns
@@ -147,124 +68,3 @@ syncedBlockPlugin.tsx ← Top-level: registers nodes, commands, UI, pm-
147
68
  2. Calls `ReferenceSyncBlockStoreManager.fetch(resourceId)` → Block Service API
148
69
  3. Renders content via nested renderer from `editor-synced-block-renderer`
149
70
  4. Subscribes to AGG WebSocket for real-time updates
150
-
151
- ---
152
-
153
- ## Common Tasks
154
-
155
- ### Implementing a new feature in sync blocks
156
-
157
- 1. **Identify scope**: Does it affect source, reference, or both? Editor, renderer, or both?
158
- 2. **Platform first**: Make changes in `editor-plugin-synced-block` or
159
- `editor-synced-block-provider`
160
- 3. **Product integration**: Update Confluence (`fabric-providers`) and/or Jira
161
- (`issue-view-synced-block-provider`)
162
- 4. **Feature gate**: Use a DnH **Experiment** (not a feature gate) for production changes. See
163
- [Experiment and gates page](https://hello.atlassian.net/wiki/spaces/egcuc/pages/6390978659)
164
- 5. **Analytics**: Add experience tracking events (see `EDITOR-1665` pattern)
165
- 6. **Test**: Add tests in the corresponding test package
166
-
167
- ### Fixing a bug
168
-
169
- 1. **Check supported node types**:
170
- [Edit at source](https://hello.atlassian.net/wiki/spaces/egcuc/pages/5926568979) |
171
- [Edit anywhere](https://hello.atlassian.net/wiki/spaces/egcuc/pages/5864526866)
172
- 2. **Check unsupported content handling**:
173
- [Unsupported content](https://hello.atlassian.net/wiki/spaces/egcuc/pages/5687277297)
174
- 3. **Debug with analytics**:
175
- [HOW-TO: Use analytics to debug errors](https://hello.atlassian.net/wiki/spaces/egcuc/pages/6342760320)
176
- 4. **Gate the fix**: Use DnH Experiment, not a feature gate
177
- 5. **Test on staging**: Verify on Hello (hello.atlassian.net) with experiment enabled
178
-
179
- ### Cleaning up a feature gate
180
-
181
- 1. Find the gate key in
182
- [Experiment and gates](https://hello.atlassian.net/wiki/spaces/egcuc/pages/6390978659)
183
- 2. Search codebase: `grep -r "gate_key_name" platform/ confluence/ jira/`
184
- 3. Remove conditional logic, keep the "enabled" code path
185
- 4. Remove the Switcheroo/Statsig configuration
186
- 5. Update the Confluence page to mark as "Cleaned up"
187
-
188
- ### Adding support for a new node type in sync blocks
189
-
190
- 1. Check if node is in the supported list:
191
- [Supported node types](https://hello.atlassian.net/wiki/spaces/egcuc/pages/5926568979)
192
- 2. For **source**: Update the allowed node schema in `editor-plugin-synced-block`
193
- 3. For **reference**: Ensure nested renderer in `editor-synced-block-renderer` can render the node
194
- 4. Test in both classic pages and live pages
195
- 5. Test in Jira issue description renderer
196
-
197
- ---
198
-
199
- ## Performance Considerations
200
-
201
- - **VC90**: Sync blocks can regress VC90 due to content shift (CLS). Reference blocks fetch content
202
- async and shift the page. Use SSR preloading to mitigate.
203
- - **SSR**: Bulk fetch endpoint reduces waterfall. Configurable via
204
- `platform_editor_sync_block_ssr_config`.
205
- - **View→Edit transition**: Cache sync block data in session storage (see
206
- `SyncBlockInMemorySessionCache`)
207
- - **Criterion tests**:
208
- [Perf test page](https://hello.atlassian.net/wiki/spaces/egcuc/pages/6498888237)
209
-
210
- ---
211
-
212
- ## Analytics Events Quick Reference
213
-
214
- **Experience events** (SLO-driving): `asyncOperation` type
215
-
216
- - `fetchSyncedBlock`, `fetchSyncedBlockSourceInfo`, `fetchSyncedBlockReferencesInfo`
217
- - `syncedBlockCreate`, `syncedBlockUpdate`, `syncedBlockDelete`
218
- - `referenceSyncedBlockUpdate`
219
-
220
- **Menu/toolbar events**: `menuAction` and `toolbarAction` types
221
-
222
- - `syncedBlockCreate` (quickInsertMenu, blockMenu, primaryToolbar)
223
- - `syncedBlockDelete`, `referenceSyncedBlockDelete` (syncedBlockToolbar)
224
- - `syncedBlockUnsync`, `referenceSyncedBlockUnsync` (syncedBlockToolbar)
225
- - `syncedBlockViewLocations`, `syncedBlockEditSource` (syncedBlockToolbar)
226
-
227
- **Standard analytics events**: Track individual sync block operations
228
-
229
- - `fetched/fetchSyncedBlock` — attributes: `resourceId`, `blockInstanceId`, `sourceProduct`
230
- - `rendered/renderSyncedBlock` — attributes: `resourceId`, `sourceProduct`, state
231
- (`loaded`/`error`/`permissionDenied`/`missingSource`)
232
- - `inserted/documentInserted` (page save) — attributes: `numberOfSyncedBlocks`,
233
- `numberOfReferencedSyncedBlocks`
234
-
235
- Full catalogue:
236
- [Synced Block Analytics Catalogue](https://hello.atlassian.net/wiki/spaces/egcuc/pages/6332253480)
237
-
238
- ---
239
-
240
- ## Key Jira Queries
241
-
242
- ```
243
- # All synced block tickets
244
- "Epic Link" in (EDITOR-1519, EDITOR-2441, EDITOR-1783, EDITOR-3936, EDITOR-3586, EDITOR-5010)
245
-
246
- # Open bugs
247
- ... AND type = Bug AND status not in (Done, "Won't do") ORDER BY priority DESC
248
-
249
- # M2 (Jira source creation)
250
- "Epic Link" = EDITOR-5010 ORDER BY created DESC
251
- ```
252
-
253
- ---
254
-
255
- ## Key Confluence Pages
256
-
257
- - **Architecture**: https://hello.atlassian.net/wiki/spaces/egcuc/pages/5505188521
258
- - **Implementation view**: https://hello.atlassian.net/wiki/spaces/egcuc/pages/5997083214
259
- - **Decisions record**: https://hello.atlassian.net/wiki/spaces/egcuc/pages/5882558842
260
- - **Feature flags**: https://hello.atlassian.net/wiki/spaces/egcuc/pages/6390978659
261
- - **Analytics catalogue**: https://hello.atlassian.net/wiki/spaces/egcuc/pages/6332253480
262
- - **Debug errors HOW-TO**: https://hello.atlassian.net/wiki/spaces/egcuc/pages/6342760320
263
-
264
- ---
265
-
266
- ## Slack Channels
267
-
268
- - **Engineering**: https://atlassian.enterprise.slack.com/archives/C09DZT1TBNW
269
- - **Product & Design**: https://atlassian.enterprise.slack.com/archives/C091Y69RBGU
270
- - **M2 / Jira**: https://atlassian.enterprise.slack.com/archives/C09RS7JCBED
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 6.0.46
4
+
5
+ ### Patch Changes
6
+
7
+ - [`20b51bc2e61a4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/20b51bc2e61a4) -
8
+ Remove duplicate source synced blocks when inserting block templates with existing resourceIds and
9
+ show error flag
10
+ - [`15deee785151b`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/15deee785151b) -
11
+ EDITOR-6174 Pass node to createBodiedSyncBlockNode to cache content on creation, preventing false
12
+ unsaved changes on page refresh
13
+ - Updated dependencies
14
+
15
+ ## 6.0.45
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies
20
+
3
21
  ## 6.0.44
4
22
 
5
23
  ### Patch Changes
@@ -17,6 +17,7 @@ var _syncBlock = require("@atlaskit/editor-common/sync-block");
17
17
  var _utils = require("@atlaskit/editor-common/utils");
18
18
  var _editorPluginConnectivity = require("@atlaskit/editor-plugin-connectivity");
19
19
  var _state = require("@atlaskit/editor-prosemirror/state");
20
+ var _transform = require("@atlaskit/editor-prosemirror/transform");
20
21
  var _view = require("@atlaskit/editor-prosemirror/view");
21
22
  var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
22
23
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
@@ -576,11 +577,85 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
576
577
  _ret = _loop();
577
578
  if (_ret) return _ret.v;
578
579
  }
580
+
581
+ // Detect and remove duplicate bodiedSyncBlock resourceIds.
582
+ // When a block template containing a source sync block is inserted into the
583
+ // same document, it creates a duplicate with the same resourceId. We keep the
584
+ // first occurrence and delete subsequent duplicates entirely (including their
585
+ // contents), since a document must not contain two source sync blocks with the
586
+ // same resourceId.
579
587
  } catch (err) {
580
588
  _iterator2.e(err);
581
589
  } finally {
582
590
  _iterator2.f();
583
591
  }
592
+ if (trs.some(function (tr) {
593
+ return tr.docChanged && !tr.getMeta('isRemote');
594
+ }) && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_8')) {
595
+ // Quick check: only walk the full document when at least one
596
+ // transaction inserted a source synced block. This avoids an
597
+ // expensive descendants() traversal on every local edit.
598
+ var hasInsertedSourceBlock = trs.some(function (tr) {
599
+ if (!tr.docChanged || tr.getMeta('isRemote')) {
600
+ return false;
601
+ }
602
+ return tr.steps.some(function (step) {
603
+ if (!(step instanceof _transform.ReplaceStep || step instanceof _transform.ReplaceAroundStep) || !('slice' in step)) {
604
+ return false;
605
+ }
606
+ var _ref0 = step,
607
+ slice = _ref0.slice;
608
+ var found = false;
609
+ slice.content.descendants(function (node) {
610
+ if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
611
+ found = true;
612
+ }
613
+ return false;
614
+ });
615
+ return found;
616
+ });
617
+ });
618
+ if (!hasInsertedSourceBlock) {
619
+ return null;
620
+ }
621
+ var seenResourceIds = new Set();
622
+ var duplicates = [];
623
+ newState.doc.descendants(function (node, pos) {
624
+ if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
625
+ if (seenResourceIds.has(node.attrs.resourceId)) {
626
+ duplicates.push({
627
+ pos: pos,
628
+ nodeSize: node.nodeSize
629
+ });
630
+ } else {
631
+ seenResourceIds.add(node.attrs.resourceId);
632
+ }
633
+ return false;
634
+ }
635
+ });
636
+ if (duplicates.length > 0) {
637
+ var tr = newState.tr;
638
+
639
+ // Delete in reverse document order so positions remain valid
640
+ for (var i = duplicates.length - 1; i >= 0; i--) {
641
+ var dup = duplicates[i];
642
+ tr.delete(dup.pos, dup.pos + dup.nodeSize);
643
+ }
644
+ tr.setMeta('addToHistory', false);
645
+ (0, _utils2.deferDispatch)(function () {
646
+ var _api$core;
647
+ api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref1) {
648
+ var tr = _ref1.tr;
649
+ return tr.setMeta(syncedBlockPluginKey, {
650
+ activeFlag: {
651
+ id: _types.FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK
652
+ }
653
+ });
654
+ });
655
+ });
656
+ return tr;
657
+ }
658
+ }
584
659
  return null;
585
660
  }
586
661
  });
@@ -99,7 +99,7 @@ var handleBodiedSyncBlockCreation = exports.handleBodiedSyncBlockCreation = func
99
99
  });
100
100
  });
101
101
  });
102
- syncBlockStore.sourceManager.createBodiedSyncBlockNode(node.attrs, function (success) {
102
+ syncBlockStore.sourceManager.createBodiedSyncBlockNode(node.attrs, node.node, function (success) {
103
103
  if (success) {
104
104
  var _api$core4, _api$core5;
105
105
  api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
@@ -14,6 +14,7 @@ var FLAG_ID = exports.FLAG_ID = /*#__PURE__*/function (FLAG_ID) {
14
14
  FLAG_ID["CANNOT_CREATE_SYNC_BLOCK"] = "cannot-create-sync-block";
15
15
  FLAG_ID["INLINE_EXTENSION_IN_SYNC_BLOCK"] = "inline-extension-in-sync-block";
16
16
  FLAG_ID["EXTENSION_IN_SYNC_BLOCK"] = "extension-in-sync-block";
17
+ FLAG_ID["DUPLICATE_SOURCE_SYNC_BLOCK"] = "duplicate-source-sync-block";
17
18
  return FLAG_ID;
18
19
  }({});
19
20
  var SYNCED_BLOCK_BUTTON_TEST_ID = exports.SYNCED_BLOCK_BUTTON_TEST_ID = {
@@ -21,7 +21,7 @@ var _types = require("../types");
21
21
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
22
22
  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; }
23
23
  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; }
24
- var flagMap = (0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, _types.FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE, {
24
+ var flagMap = (0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, _types.FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE, {
25
25
  title: _messages.syncBlockMessages.failToDeleteTitle,
26
26
  description: _messages.syncBlockMessages.failToDeleteWhenOfflineDescription,
27
27
  type: 'error'
@@ -56,6 +56,10 @@ var flagMap = (0, _defineProperty2.default)((0, _defineProperty2.default)((0, _d
56
56
  title: _messages.syncBlockMessages.inlineExtensionInSyncBlockTitle,
57
57
  description: _messages.syncBlockMessages.inlineExtensionInSyncBlockDescription,
58
58
  type: 'error'
59
+ }), _types.FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK, {
60
+ title: _messages.syncBlockMessages.duplicateSourceSyncBlockTitle,
61
+ description: _messages.syncBlockMessages.duplicateSourceSyncBlockDescription,
62
+ type: 'error'
59
63
  });
60
64
  var Flag = exports.Flag = function Flag(_ref) {
61
65
  var api = _ref.api;
@@ -7,6 +7,7 @@ import { BodiedSyncBlockSharedCssClassName, SyncBlockStateCssClassName } from '@
7
7
  import { mapSlice, pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
8
8
  import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
9
9
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
10
+ import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
10
11
  import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
11
12
  import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
12
13
  import { fg } from '@atlaskit/platform-feature-flags';
@@ -526,6 +527,80 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
526
527
  }
527
528
  }
528
529
  }
530
+
531
+ // Detect and remove duplicate bodiedSyncBlock resourceIds.
532
+ // When a block template containing a source sync block is inserted into the
533
+ // same document, it creates a duplicate with the same resourceId. We keep the
534
+ // first occurrence and delete subsequent duplicates entirely (including their
535
+ // contents), since a document must not contain two source sync blocks with the
536
+ // same resourceId.
537
+ if (trs.some(tr => tr.docChanged && !tr.getMeta('isRemote')) && fg('platform_synced_block_patch_8')) {
538
+ // Quick check: only walk the full document when at least one
539
+ // transaction inserted a source synced block. This avoids an
540
+ // expensive descendants() traversal on every local edit.
541
+ const hasInsertedSourceBlock = trs.some(tr => {
542
+ if (!tr.docChanged || tr.getMeta('isRemote')) {
543
+ return false;
544
+ }
545
+ return tr.steps.some(step => {
546
+ if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep) || !('slice' in step)) {
547
+ return false;
548
+ }
549
+ const {
550
+ slice
551
+ } = step;
552
+ let found = false;
553
+ slice.content.descendants(node => {
554
+ if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
555
+ found = true;
556
+ }
557
+ return false;
558
+ });
559
+ return found;
560
+ });
561
+ });
562
+ if (!hasInsertedSourceBlock) {
563
+ return null;
564
+ }
565
+ const seenResourceIds = new Set();
566
+ const duplicates = [];
567
+ newState.doc.descendants((node, pos) => {
568
+ if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
569
+ if (seenResourceIds.has(node.attrs.resourceId)) {
570
+ duplicates.push({
571
+ pos,
572
+ nodeSize: node.nodeSize
573
+ });
574
+ } else {
575
+ seenResourceIds.add(node.attrs.resourceId);
576
+ }
577
+ return false;
578
+ }
579
+ });
580
+ if (duplicates.length > 0) {
581
+ const {
582
+ tr
583
+ } = newState;
584
+
585
+ // Delete in reverse document order so positions remain valid
586
+ for (let i = duplicates.length - 1; i >= 0; i--) {
587
+ const dup = duplicates[i];
588
+ tr.delete(dup.pos, dup.pos + dup.nodeSize);
589
+ }
590
+ tr.setMeta('addToHistory', false);
591
+ deferDispatch(() => {
592
+ var _api$core;
593
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
594
+ tr
595
+ }) => tr.setMeta(syncedBlockPluginKey, {
596
+ activeFlag: {
597
+ id: FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK
598
+ }
599
+ }));
600
+ });
601
+ return tr;
602
+ }
603
+ }
529
604
  return null;
530
605
  }
531
606
  });
@@ -91,7 +91,7 @@ export const handleBodiedSyncBlockCreation = (bodiedSyncBlockAdded, editorState,
91
91
  });
92
92
  });
93
93
  });
94
- syncBlockStore.sourceManager.createBodiedSyncBlockNode(node.attrs, success => {
94
+ syncBlockStore.sourceManager.createBodiedSyncBlockNode(node.attrs, node.node, success => {
95
95
  if (success) {
96
96
  var _api$core4, _api$core5;
97
97
  api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions.execute(({
@@ -8,6 +8,7 @@ export let FLAG_ID = /*#__PURE__*/function (FLAG_ID) {
8
8
  FLAG_ID["CANNOT_CREATE_SYNC_BLOCK"] = "cannot-create-sync-block";
9
9
  FLAG_ID["INLINE_EXTENSION_IN_SYNC_BLOCK"] = "inline-extension-in-sync-block";
10
10
  FLAG_ID["EXTENSION_IN_SYNC_BLOCK"] = "extension-in-sync-block";
11
+ FLAG_ID["DUPLICATE_SOURCE_SYNC_BLOCK"] = "duplicate-source-sync-block";
11
12
  return FLAG_ID;
12
13
  }({});
13
14
  export const SYNCED_BLOCK_BUTTON_TEST_ID = {
@@ -53,6 +53,11 @@ const flagMap = {
53
53
  title: messages.inlineExtensionInSyncBlockTitle,
54
54
  description: messages.inlineExtensionInSyncBlockDescription,
55
55
  type: 'error'
56
+ },
57
+ [FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK]: {
58
+ title: messages.duplicateSourceSyncBlockTitle,
59
+ description: messages.duplicateSourceSyncBlockDescription,
60
+ type: 'error'
56
61
  }
57
62
  };
58
63
  export const Flag = ({
@@ -15,6 +15,7 @@ import { BodiedSyncBlockSharedCssClassName, SyncBlockStateCssClassName } from '@
15
15
  import { mapSlice, pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
16
16
  import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
17
17
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
18
+ import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
18
19
  import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
19
20
  import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
20
21
  import { fg } from '@atlaskit/platform-feature-flags';
@@ -569,11 +570,85 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
569
570
  _ret = _loop();
570
571
  if (_ret) return _ret.v;
571
572
  }
573
+
574
+ // Detect and remove duplicate bodiedSyncBlock resourceIds.
575
+ // When a block template containing a source sync block is inserted into the
576
+ // same document, it creates a duplicate with the same resourceId. We keep the
577
+ // first occurrence and delete subsequent duplicates entirely (including their
578
+ // contents), since a document must not contain two source sync blocks with the
579
+ // same resourceId.
572
580
  } catch (err) {
573
581
  _iterator2.e(err);
574
582
  } finally {
575
583
  _iterator2.f();
576
584
  }
585
+ if (trs.some(function (tr) {
586
+ return tr.docChanged && !tr.getMeta('isRemote');
587
+ }) && fg('platform_synced_block_patch_8')) {
588
+ // Quick check: only walk the full document when at least one
589
+ // transaction inserted a source synced block. This avoids an
590
+ // expensive descendants() traversal on every local edit.
591
+ var hasInsertedSourceBlock = trs.some(function (tr) {
592
+ if (!tr.docChanged || tr.getMeta('isRemote')) {
593
+ return false;
594
+ }
595
+ return tr.steps.some(function (step) {
596
+ if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep) || !('slice' in step)) {
597
+ return false;
598
+ }
599
+ var _ref0 = step,
600
+ slice = _ref0.slice;
601
+ var found = false;
602
+ slice.content.descendants(function (node) {
603
+ if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
604
+ found = true;
605
+ }
606
+ return false;
607
+ });
608
+ return found;
609
+ });
610
+ });
611
+ if (!hasInsertedSourceBlock) {
612
+ return null;
613
+ }
614
+ var seenResourceIds = new Set();
615
+ var duplicates = [];
616
+ newState.doc.descendants(function (node, pos) {
617
+ if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
618
+ if (seenResourceIds.has(node.attrs.resourceId)) {
619
+ duplicates.push({
620
+ pos: pos,
621
+ nodeSize: node.nodeSize
622
+ });
623
+ } else {
624
+ seenResourceIds.add(node.attrs.resourceId);
625
+ }
626
+ return false;
627
+ }
628
+ });
629
+ if (duplicates.length > 0) {
630
+ var tr = newState.tr;
631
+
632
+ // Delete in reverse document order so positions remain valid
633
+ for (var i = duplicates.length - 1; i >= 0; i--) {
634
+ var dup = duplicates[i];
635
+ tr.delete(dup.pos, dup.pos + dup.nodeSize);
636
+ }
637
+ tr.setMeta('addToHistory', false);
638
+ deferDispatch(function () {
639
+ var _api$core;
640
+ api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref1) {
641
+ var tr = _ref1.tr;
642
+ return tr.setMeta(syncedBlockPluginKey, {
643
+ activeFlag: {
644
+ id: FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK
645
+ }
646
+ });
647
+ });
648
+ });
649
+ return tr;
650
+ }
651
+ }
577
652
  return null;
578
653
  }
579
654
  });
@@ -92,7 +92,7 @@ export var handleBodiedSyncBlockCreation = function handleBodiedSyncBlockCreatio
92
92
  });
93
93
  });
94
94
  });
95
- syncBlockStore.sourceManager.createBodiedSyncBlockNode(node.attrs, function (success) {
95
+ syncBlockStore.sourceManager.createBodiedSyncBlockNode(node.attrs, node.node, function (success) {
96
96
  if (success) {
97
97
  var _api$core4, _api$core5;
98
98
  api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref3) {
@@ -8,6 +8,7 @@ export var FLAG_ID = /*#__PURE__*/function (FLAG_ID) {
8
8
  FLAG_ID["CANNOT_CREATE_SYNC_BLOCK"] = "cannot-create-sync-block";
9
9
  FLAG_ID["INLINE_EXTENSION_IN_SYNC_BLOCK"] = "inline-extension-in-sync-block";
10
10
  FLAG_ID["EXTENSION_IN_SYNC_BLOCK"] = "extension-in-sync-block";
11
+ FLAG_ID["DUPLICATE_SOURCE_SYNC_BLOCK"] = "duplicate-source-sync-block";
11
12
  return FLAG_ID;
12
13
  }({});
13
14
  export var SYNCED_BLOCK_BUTTON_TEST_ID = {
@@ -12,7 +12,7 @@ import StatusSuccessIcon from '@atlaskit/icon/core/status-success';
12
12
  import StatusWarningIcon from '@atlaskit/icon/core/status-warning';
13
13
  import { syncedBlockPluginKey } from '../pm-plugins/main';
14
14
  import { FLAG_ID } from '../types';
15
- var flagMap = _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty({}, FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE, {
15
+ var flagMap = _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty({}, FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE, {
16
16
  title: messages.failToDeleteTitle,
17
17
  description: messages.failToDeleteWhenOfflineDescription,
18
18
  type: 'error'
@@ -47,6 +47,10 @@ var flagMap = _defineProperty(_defineProperty(_defineProperty(_defineProperty(_d
47
47
  title: messages.inlineExtensionInSyncBlockTitle,
48
48
  description: messages.inlineExtensionInSyncBlockDescription,
49
49
  type: 'error'
50
+ }), FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK, {
51
+ title: messages.duplicateSourceSyncBlockTitle,
52
+ description: messages.duplicateSourceSyncBlockDescription,
53
+ type: 'error'
50
54
  });
51
55
  export var Flag = function Flag(_ref) {
52
56
  var api = _ref.api;
@@ -10,7 +10,8 @@ export declare enum FLAG_ID {
10
10
  UNPUBLISHED_SYNC_BLOCK_PASTED = "unpublished-sync-block-pasted",
11
11
  CANNOT_CREATE_SYNC_BLOCK = "cannot-create-sync-block",
12
12
  INLINE_EXTENSION_IN_SYNC_BLOCK = "inline-extension-in-sync-block",
13
- EXTENSION_IN_SYNC_BLOCK = "extension-in-sync-block"
13
+ EXTENSION_IN_SYNC_BLOCK = "extension-in-sync-block",
14
+ DUPLICATE_SOURCE_SYNC_BLOCK = "duplicate-source-sync-block"
14
15
  }
15
16
  type FlagConfig = {
16
17
  id: FLAG_ID;
@@ -10,7 +10,8 @@ export declare enum FLAG_ID {
10
10
  UNPUBLISHED_SYNC_BLOCK_PASTED = "unpublished-sync-block-pasted",
11
11
  CANNOT_CREATE_SYNC_BLOCK = "cannot-create-sync-block",
12
12
  INLINE_EXTENSION_IN_SYNC_BLOCK = "inline-extension-in-sync-block",
13
- EXTENSION_IN_SYNC_BLOCK = "extension-in-sync-block"
13
+ EXTENSION_IN_SYNC_BLOCK = "extension-in-sync-block",
14
+ DUPLICATE_SOURCE_SYNC_BLOCK = "duplicate-source-sync-block"
14
15
  }
15
16
  type FlagConfig = {
16
17
  id: FLAG_ID;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "6.0.44",
3
+ "version": "6.0.46",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -29,8 +29,8 @@
29
29
  "atlaskit:src": "src/index.ts",
30
30
  "dependencies": {
31
31
  "@atlaskit/adf-schema": "^52.4.0",
32
- "@atlaskit/button": "23.10.9",
33
- "@atlaskit/dropdown-menu": "16.8.5",
32
+ "@atlaskit/button": "23.10.10",
33
+ "@atlaskit/dropdown-menu": "16.8.6",
34
34
  "@atlaskit/editor-json-transformer": "^8.31.0",
35
35
  "@atlaskit/editor-plugin-analytics": "^8.0.0",
36
36
  "@atlaskit/editor-plugin-block-menu": "^7.0.0",
@@ -46,16 +46,16 @@
46
46
  "@atlaskit/editor-synced-block-provider": "^4.3.0",
47
47
  "@atlaskit/editor-toolbar": "^0.20.0",
48
48
  "@atlaskit/flag": "^17.9.0",
49
- "@atlaskit/icon": "34.0.1",
50
- "@atlaskit/icon-lab": "^6.3.0",
49
+ "@atlaskit/icon": "34.0.2",
50
+ "@atlaskit/icon-lab": "^6.4.0",
51
51
  "@atlaskit/logo": "^19.10.0",
52
52
  "@atlaskit/lozenge": "^13.5.0",
53
53
  "@atlaskit/modal-dialog": "^14.14.0",
54
54
  "@atlaskit/platform-feature-flags": "^1.1.0",
55
55
  "@atlaskit/primitives": "^18.1.0",
56
- "@atlaskit/spinner": "19.0.13",
57
- "@atlaskit/tmp-editor-statsig": "^54.1.0",
58
- "@atlaskit/tokens": "11.4.2",
56
+ "@atlaskit/spinner": "19.0.14",
57
+ "@atlaskit/tmp-editor-statsig": "^54.4.0",
58
+ "@atlaskit/tokens": "11.4.3",
59
59
  "@atlaskit/tooltip": "^21.1.0",
60
60
  "@atlaskit/visually-hidden": "^3.0.0",
61
61
  "@babel/runtime": "^7.0.0",
@@ -65,7 +65,7 @@
65
65
  "react-intl-next": "npm:react-intl@^5.18.1"
66
66
  },
67
67
  "peerDependencies": {
68
- "@atlaskit/editor-common": "^112.16.0",
68
+ "@atlaskit/editor-common": "^112.18.0",
69
69
  "react": "^18.2.0"
70
70
  },
71
71
  "devDependencies": {