@atlaskit/editor-synced-block-provider 4.3.1 → 4.3.3

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,9 +1,9 @@
1
1
  # Synced Block Provider — Developer Agent Guide
2
2
 
3
- > **Package**: `@atlaskit/editor-synced-block-provider` **Purpose**: Data layer for synced blocks —
4
- > store managers, block service API client, ARI generation, permissions, media tokens. **Full
5
- > Knowledge Base**:
6
- > [Synced Blocks — Comprehensive Knowledge Base](https://hello.atlassian.net/wiki/spaces/egcuc/pages/6679548384)
3
+ > **Package**: `@atlaskit/editor-synced-block-provider`
4
+ >
5
+ > **For workflow guidance, debugging, and cross-package task guides, load the `synced-blocks` skill:**
6
+ > `get_skill(skill_name_or_path="platform/packages/editor/.rovodev/skills/synced-blocks/SKILL.md")`
7
7
 
8
8
  ---
9
9
 
@@ -19,29 +19,29 @@ plugin and the renderer across Confluence and Jira.
19
19
 
20
20
  ```
21
21
  src/
22
- ├── index.ts Barrel export
22
+ ├── index.ts # Barrel export
23
23
  ├── store-manager/
24
- │ ├── syncBlockStoreManager.ts Parent coordinator for source + reference managers
25
- │ ├── referenceSyncBlockStoreManager.ts Reference block lifecycle, cache, subscriptions, flush
26
- │ └── sourceSyncBlockStoreManager.ts Source block create, update, delete, flush
24
+ │ ├── syncBlockStoreManager.ts # Parent coordinator for source + reference managers
25
+ │ ├── referenceSyncBlockStoreManager.ts # Reference block lifecycle, cache, subscriptions, flush
26
+ │ └── sourceSyncBlockStoreManager.ts # Source block create, update, delete, flush
27
27
  ├── clients/
28
28
  │ ├── block-service/
29
- │ │ ├── blockService.ts Block service API client (fetch, batch, CRUD)
30
- │ │ └── ari.ts Block ARI generation/parsing
29
+ │ │ ├── blockService.ts # Block service API client (fetch, batch, CRUD)
30
+ │ │ └── ari.ts # Block ARI generation/parsing
31
31
  │ ├── confluence/
32
- │ │ ├── ari.ts Confluence page ARI generation/parsing
33
- │ │ └── fetchMediaToken.ts Media token fetching via GraphQL (MediaUploadTokenQuery)
32
+ │ │ ├── ari.ts # Confluence page ARI generation/parsing
33
+ │ │ └── fetchMediaToken.ts # Media token fetching via GraphQL (MediaUploadTokenQuery)
34
34
  │ └── jira/
35
- │ └── ari.ts Jira work item ARI generation/parsing
35
+ │ └── ari.ts # Jira work item ARI generation/parsing
36
36
  ├── providers/
37
37
  │ └── block-service/
38
- │ └── blockServiceAPI.ts Provider factory and API helpers
39
- └── types/ Shared types
38
+ │ └── blockServiceAPI.ts # Provider factory and API helpers
39
+ └── types/ # Shared types
40
40
  ```
41
41
 
42
42
  ---
43
43
 
44
- ## Key Concepts
44
+ ## Key Exports and Types
45
45
 
46
46
  ### Store Manager Hierarchy
47
47
 
@@ -49,81 +49,41 @@ src/
49
49
  SyncBlockStoreManager (parent coordinator)
50
50
  ├── SourceSyncBlockStoreManager
51
51
  │ ├── create(content) → Block Service API → returns resourceId
52
- │ ├── update(resourceId, content) → debounced 3s write
53
- │ ├── delete(resourceId) → soft delete
54
- └── flush() → persist all pending changes on page save
52
+ │ ├── updateSyncBlockData(node) → marks isDirty, caches content
53
+ │ ├── flush() → persist all dirty changes to backend
54
+ ├── hasUnsavedChanges() → checks isDirty + hasReceivedContentChange
55
+ │ └── delete(resourceId) → soft delete with confirmation
55
56
  └── ReferenceSyncBlockStoreManager
56
57
  ├── fetchSyncBlocksData(nodes) → batch fetch with deduplication
57
- ├── subscribeToSyncBlock(resourceId, localId, callback) → AGG WebSocket
58
+ ├── subscribeToSyncBlock(resourceId, callback) → AGG WebSocket
58
59
  ├── fetchSyncBlockSourceInfo(resourceId) → title, URL metadata
59
- ├── getFromCache(resourceId) → retrieve cached data
60
- ├── flush() → save reference changes to backend
61
60
  └── destroy() → cleanup subscriptions and batchers
62
61
  ```
63
62
 
64
- ### ARI Formats & Utilities
63
+ ### ARI Utilities
65
64
 
66
- | Function | ARI Pattern | Example |
67
- | ------------------------------------------------------------ | ------------------------- | --------------------------------------------------------------- |
68
- | `generateBlockAri({cloudId, parentId, product, resourceId})` | Source block ARI | `ari:cloud:block::{cloudId}/confluence-page:{pageId}/{localId}` |
69
- | `generateBlockAriFromReference({cloudId, resourceId})` | Reference block ARI | — |
70
- | `getConfluencePageAri({pageId, cloudId, pageType})` | Confluence page | `ari:cloud:confluence::{cloudId}:page/{pageId}` |
71
- | `getJiraWorkItemAri({cloudId, workItemId})` | Jira issue | `ari:cloud:jira::{cloudId}:work-item/{issueId}` |
72
- | `getJiraWorkItemIdFromAri(ari)` | Extract issue ID from ARI | — |
65
+ | Function | Purpose |
66
+ | ------------------------------------------------------------ | -------------------------- |
67
+ | `generateBlockAri({cloudId, parentId, product, resourceId})` | Generate source block ARI |
68
+ | `generateBlockAriFromReference({cloudId, resourceId})` | Generate reference ARI |
69
+ | `getConfluencePageAri({pageId, cloudId, pageType})` | Confluence page ARI |
70
+ | `getJiraWorkItemAri({cloudId, workItemId})` | Jira issue ARI |
73
71
 
74
72
  ### Block Service API
75
73
 
76
74
  The client in `clients/block-service/blockService.ts` communicates via GraphQL at
77
- `/gateway/api/graphql`:
78
-
79
- - **Fetch**: Single or batch block content retrieval
80
- - **Create**: Register new source block with content
81
- - **Update**: Push content changes (debounced 3s)
82
- - **Delete**: Soft delete a source block
83
- - **Source Info**: Fetch metadata (source page title, URL)
84
- - **References Info**: Fetch list of locations referencing a block
75
+ `/gateway/api/graphql`: Fetch, Create, Update (debounced 3s), Delete, Source Info, References Info.
85
76
 
86
77
  ### Media Token Fetching
87
78
 
88
79
  `fetchMediaToken(contentId)` → GraphQL `MediaUploadTokenQuery` → returns
89
80
  `{token, config: {clientId, fileStoreUrl}, collectionId}`
90
81
 
91
- Used when synced blocks contain media (images, files) — the reference needs a valid token to render
92
- media from the source page.
93
-
94
- ---
95
-
96
- ## Common Tasks
97
-
98
- ### Adding a new API method
99
-
100
- 1. Add the GraphQL query/mutation in `clients/block-service/blockService.ts`
101
- 2. Expose it through the appropriate store manager
102
- 3. Export from `src/index.ts` if needed by product integrations
103
- 4. Add tests in `editor-synced-block-provider-tests`
104
-
105
- ### Debugging data issues
106
-
107
- 1. Check `ReferenceSyncBlockStoreManager` cache state
108
- 2. Verify ARI format matches expected pattern for the product
109
- 3. Check Block Service API responses in network tab (look for `/gateway/api/graphql`)
110
- 4. Use analytics:
111
- [HOW-TO: Debug errors](https://hello.atlassian.net/wiki/spaces/egcuc/pages/6342760320)
112
-
113
- ### Adding support for a new product
114
-
115
- 1. Create ARI utilities in `clients/{product}/ari.ts`
116
- 2. Ensure `generateBlockAri` supports the new product type
117
- 3. Add media token fetching if the product has media content
118
- 4. Update store managers if the product has unique lifecycle requirements
119
-
120
82
  ---
121
83
 
122
84
  ## Related Packages
123
85
 
124
- - **Plugin**: `platform/packages/editor/editor-plugin-synced-block/` — uses store managers
125
- - **Renderer**: `platform/packages/editor/editor-synced-block-renderer/` — uses fetch provider
126
- - **Confluence**: `confluence/next/packages/fabric-providers/src/SyncedBlockProvider.ts` — wraps
127
- this provider
128
- - **Jira**: `jira/src/packages/issue/issue-view-synced-block-provider/` — wraps this provider with
129
- Relay
86
+ - **Plugin**: `platform/packages/editor/editor-plugin-synced-block/`
87
+ - **Renderer**: `platform/packages/editor/editor-synced-block-renderer/`
88
+ - **Confluence**: `confluence/next/packages/fabric-providers/src/SyncedBlockProvider.ts`
89
+ - **Jira**: `jira/src/packages/issue/issue-view-synced-block-provider/`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @atlaskit/editor-synced-block-provider
2
2
 
3
+ ## 4.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`15deee785151b`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/15deee785151b) -
8
+ EDITOR-6174 Pass node to createBodiedSyncBlockNode to cache content on creation, preventing false
9
+ unsaved changes on page refresh
10
+ - Updated dependencies
11
+
12
+ ## 4.3.2
13
+
14
+ ### Patch Changes
15
+
16
+ - [`eaeabd907b737`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/eaeabd907b737) -
17
+ Move unregistered references logic into a separate method.
18
+ - [`38e89dd6515da`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/38e89dd6515da) -
19
+ Disable synced block write operations (create, update, delete, flush) when the editor is in view
20
+ mode, gated behind platform_synced_block_patch_8. This prevents unintended block service mutations
21
+ (e.g. deletion modals, data loss) when the editor is used in read-only contexts such as Version
22
+ History.
23
+
3
24
  ## 4.3.1
4
25
 
5
26
  ### Patch Changes
@@ -13,6 +13,7 @@ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/creat
13
13
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
14
14
  var _isEqual = _interopRequireDefault(require("lodash/isEqual"));
15
15
  var _monitoring = require("@atlaskit/editor-common/monitoring");
16
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
16
17
  var _types = require("../common/types");
17
18
  var _errorHandling = require("../utils/errorHandling");
18
19
  var _experienceTracking = require("../utils/experienceTracking");
@@ -37,7 +38,7 @@ var CACHE_KEY_PREFIX = 'sync-block-data-';
37
38
  var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
38
39
  // Track the setTimeout handle for queued flush so we can cancel it on destroy
39
40
 
40
- function ReferenceSyncBlockStoreManager(dataProvider) {
41
+ function ReferenceSyncBlockStoreManager(dataProvider, viewMode) {
41
42
  var _this = this,
42
43
  _this$dataProvider;
43
44
  (0, _classCallCheck2.default)(this, ReferenceSyncBlockStoreManager);
@@ -52,6 +53,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
52
53
  // Track if another flush is needed after the current one completes
53
54
  (0, _defineProperty2.default)(this, "flushNeededAfterCurrent", false);
54
55
  this.dataProvider = dataProvider;
56
+ this.viewMode = viewMode;
55
57
  this.syncBlockFetchDataRequests = new Map();
56
58
  this.syncBlockSourceInfoRequests = new Map();
57
59
  this.newlyAddedSyncBlocks = new Set();
@@ -702,14 +704,20 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
702
704
  return _regenerator.default.wrap(function _callee3$(_context4) {
703
705
  while (1) switch (_context4.prev = _context4.next) {
704
706
  case 0:
705
- if (this.isCacheDirty) {
707
+ if (!(this.viewMode === 'view' && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_8'))) {
706
708
  _context4.next = 2;
707
709
  break;
708
710
  }
709
- return _context4.abrupt("return", true);
711
+ return _context4.abrupt("return", false);
710
712
  case 2:
713
+ if (this.isCacheDirty) {
714
+ _context4.next = 4;
715
+ break;
716
+ }
717
+ return _context4.abrupt("return", true);
718
+ case 4:
711
719
  if (!this.isFlushInProgress) {
712
- _context4.next = 7;
720
+ _context4.next = 9;
713
721
  break;
714
722
  }
715
723
  // Mark that another flush is needed after the current one completes
@@ -717,22 +725,22 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
717
725
 
718
726
  // We return true here because we know the pending flush will handle the dirty cache
719
727
  return _context4.abrupt("return", true);
720
- case 7:
728
+ case 9:
721
729
  this.isFlushInProgress = true;
722
- case 8:
730
+ case 10:
723
731
  success = true; // a copy of the subscriptions STRUCTURE (without the callbacks)
724
732
  // To be saved as the last flushed structure if the flush is successful
725
733
  syncedBlocksToFlush = {};
726
- _context4.prev = 10;
734
+ _context4.prev = 12;
727
735
  if (this.dataProvider) {
728
- _context4.next = 13;
736
+ _context4.next = 15;
729
737
  break;
730
738
  }
731
739
  throw new Error('Data provider not set');
732
- case 13:
740
+ case 15:
733
741
  blocks = []; // First, build the complete subscription structure
734
742
  _iterator = _createForOfIteratorHelper(this._subscriptionManager.getSubscriptions().entries());
735
- _context4.prev = 15;
743
+ _context4.prev = 17;
736
744
  _loop = /*#__PURE__*/_regenerator.default.mark(function _loop() {
737
745
  var _step$value, resourceId, callbacks;
738
746
  return _regenerator.default.wrap(function _loop$(_context3) {
@@ -754,34 +762,34 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
754
762
  }, _loop);
755
763
  });
756
764
  _iterator.s();
757
- case 18:
765
+ case 20:
758
766
  if ((_step = _iterator.n()).done) {
759
- _context4.next = 22;
767
+ _context4.next = 24;
760
768
  break;
761
769
  }
762
- return _context4.delegateYield(_loop(), "t0", 20);
763
- case 20:
764
- _context4.next = 18;
765
- break;
770
+ return _context4.delegateYield(_loop(), "t0", 22);
766
771
  case 22:
767
- _context4.next = 27;
772
+ _context4.next = 20;
768
773
  break;
769
774
  case 24:
770
- _context4.prev = 24;
771
- _context4.t1 = _context4["catch"](15);
775
+ _context4.next = 29;
776
+ break;
777
+ case 26:
778
+ _context4.prev = 26;
779
+ _context4.t1 = _context4["catch"](17);
772
780
  _iterator.e(_context4.t1);
773
- case 27:
774
- _context4.prev = 27;
781
+ case 29:
782
+ _context4.prev = 29;
775
783
  _iterator.f();
776
- return _context4.finish(27);
777
- case 30:
784
+ return _context4.finish(29);
785
+ case 32:
778
786
  if (!(0, _isEqual.default)(syncedBlocksToFlush, this.lastFlushedSyncedBlocks)) {
779
- _context4.next = 33;
787
+ _context4.next = 35;
780
788
  break;
781
789
  }
782
790
  this.isCacheDirty = false; // Reset since we're considering this a successful no-op flush
783
791
  return _context4.abrupt("return", true);
784
- case 33:
792
+ case 35:
785
793
  // reset isCacheDirty early to prevent race condition
786
794
  // There is a race condition where if a user makes changes (create/delete) to a reference sync block
787
795
  // on a live page and the reference sync block is being saved while the user
@@ -789,9 +797,9 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
789
797
  // exactly at a time when the updateReferenceData is being executed asynchronously.
790
798
  this.isCacheDirty = false;
791
799
  (_this$saveExperience = this.saveExperience) === null || _this$saveExperience === void 0 || _this$saveExperience.start();
792
- _context4.next = 37;
800
+ _context4.next = 39;
793
801
  return this.dataProvider.updateReferenceData(blocks);
794
- case 37:
802
+ case 39:
795
803
  updateResult = _context4.sent;
796
804
  if (!updateResult.success) {
797
805
  success = false;
@@ -800,11 +808,11 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
800
808
  });
801
809
  (_this$fireAnalyticsEv6 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv6 === void 0 || _this$fireAnalyticsEv6.call(this, (0, _errorHandling.updateReferenceErrorPayload)(updateResult.error || 'Failed to update reference synced blocks on the document'));
802
810
  }
803
- _context4.next = 47;
811
+ _context4.next = 49;
804
812
  break;
805
- case 41:
806
- _context4.prev = 41;
807
- _context4.t2 = _context4["catch"](10);
813
+ case 43:
814
+ _context4.prev = 43;
815
+ _context4.t2 = _context4["catch"](12);
808
816
  success = false;
809
817
  (0, _monitoring.logException)(_context4.t2, {
810
818
  location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
@@ -813,8 +821,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
813
821
  reason: _context4.t2.message
814
822
  });
815
823
  (_this$fireAnalyticsEv7 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv7 === void 0 || _this$fireAnalyticsEv7.call(this, (0, _errorHandling.updateReferenceErrorPayload)(_context4.t2.message));
816
- case 47:
817
- _context4.prev = 47;
824
+ case 49:
825
+ _context4.prev = 49;
818
826
  if (!success) {
819
827
  // set isCacheDirty back to true for cases where it failed to update the reference synced blocks on the BE
820
828
  this.isCacheDirty = true;
@@ -836,14 +844,14 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
836
844
  void _this7.flush();
837
845
  }, 0);
838
846
  }
839
- return _context4.finish(47);
840
- case 52:
847
+ return _context4.finish(49);
848
+ case 54:
841
849
  return _context4.abrupt("return", success);
842
- case 53:
850
+ case 55:
843
851
  case "end":
844
852
  return _context4.stop();
845
853
  }
846
- }, _callee3, this, [[10, 41, 47, 52], [15, 24, 27, 30]]);
854
+ }, _callee3, this, [[12, 43, 49, 54], [17, 26, 29, 32]]);
847
855
  }));
848
856
  function flush() {
849
857
  return _flush.apply(this, arguments);