@atlaskit/editor-synced-block-provider 3.12.1 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/clients/block-service/blockSubscription.js +124 -0
  3. package/dist/cjs/clients/jira/sourceInfo.js +152 -0
  4. package/dist/cjs/providers/block-service/blockServiceAPI.js +43 -6
  5. package/dist/cjs/providers/syncBlockProvider.js +40 -8
  6. package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +347 -114
  7. package/dist/cjs/store-manager/syncBlockStoreManager.js +2 -2
  8. package/dist/cjs/utils/resolveSyncBlockInstance.js +1 -1
  9. package/dist/es2019/clients/block-service/blockSubscription.js +125 -0
  10. package/dist/es2019/clients/jira/sourceInfo.js +87 -0
  11. package/dist/es2019/providers/block-service/blockServiceAPI.js +40 -5
  12. package/dist/es2019/providers/syncBlockProvider.js +26 -2
  13. package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +233 -45
  14. package/dist/es2019/store-manager/syncBlockStoreManager.js +2 -2
  15. package/dist/es2019/utils/resolveSyncBlockInstance.js +1 -1
  16. package/dist/esm/clients/block-service/blockSubscription.js +118 -0
  17. package/dist/esm/clients/jira/sourceInfo.js +147 -0
  18. package/dist/esm/providers/block-service/blockServiceAPI.js +43 -6
  19. package/dist/esm/providers/syncBlockProvider.js +38 -6
  20. package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +347 -114
  21. package/dist/esm/store-manager/syncBlockStoreManager.js +2 -2
  22. package/dist/esm/utils/resolveSyncBlockInstance.js +1 -1
  23. package/dist/types/clients/block-service/blockService.d.ts +2 -2
  24. package/dist/types/clients/block-service/blockSubscription.d.ts +38 -0
  25. package/dist/types/clients/jira/sourceInfo.d.ts +2 -0
  26. package/dist/types/common/types.d.ts +4 -2
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/providers/block-service/blockServiceAPI.d.ts +8 -0
  29. package/dist/types/providers/syncBlockProvider.d.ts +9 -1
  30. package/dist/types/providers/types.d.ts +22 -6
  31. package/dist/types/store-manager/referenceSyncBlockStoreManager.d.ts +59 -0
  32. package/dist/types-ts4.5/clients/block-service/blockService.d.ts +2 -2
  33. package/dist/types-ts4.5/clients/block-service/blockSubscription.d.ts +38 -0
  34. package/dist/types-ts4.5/clients/jira/sourceInfo.d.ts +2 -0
  35. package/dist/types-ts4.5/common/types.d.ts +4 -2
  36. package/dist/types-ts4.5/index.d.ts +2 -2
  37. package/dist/types-ts4.5/providers/block-service/blockServiceAPI.d.ts +8 -0
  38. package/dist/types-ts4.5/providers/syncBlockProvider.d.ts +9 -1
  39. package/dist/types-ts4.5/providers/types.d.ts +22 -6
  40. package/dist/types-ts4.5/store-manager/referenceSyncBlockStoreManager.d.ts +59 -0
  41. package/package.json +2 -1
@@ -89,7 +89,7 @@ var SyncBlockStoreManager = exports.SyncBlockStoreManager = /*#__PURE__*/functio
89
89
  return _context.abrupt("return", undefined);
90
90
  case 5:
91
91
  return _context.abrupt("return", _objectSpread(_objectSpread({}, sourceInfo), {}, {
92
- onSamePage: reference.onSamePage,
92
+ onSameDocument: reference.onSameDocument,
93
93
  hasAccess: reference.hasAccess,
94
94
  productType: sourceInfo.productType
95
95
  }));
@@ -113,7 +113,7 @@ var SyncBlockStoreManager = exports.SyncBlockStoreManager = /*#__PURE__*/functio
113
113
  sourceSyncBlockData = _context2.sent;
114
114
  if (sourceSyncBlockData) {
115
115
  sourceInfos.push(_objectSpread(_objectSpread({}, sourceSyncBlockData), {}, {
116
- onSamePage: Boolean(sourceSyncBlockData === null || sourceSyncBlockData === void 0 ? void 0 : sourceSyncBlockData.onSamePage),
116
+ onSameDocument: Boolean(sourceSyncBlockData === null || sourceSyncBlockData === void 0 ? void 0 : sourceSyncBlockData.onSameDocument),
117
117
  hasAccess: true,
118
118
  isSource: true,
119
119
  productType: sourceSyncBlockData === null || sourceSyncBlockData === void 0 ? void 0 : sourceSyncBlockData.productType
@@ -41,7 +41,7 @@ var resolveSyncBlockInstance = exports.resolveSyncBlockInstance = function resol
41
41
  sourceTitle: ((_newResult$data2 = newResult.data) === null || _newResult$data2 === void 0 ? void 0 : _newResult$data2.sourceTitle) || ((_oldResult$data2 = oldResult.data) === null || _oldResult$data2 === void 0 ? void 0 : _oldResult$data2.sourceTitle) || undefined
42
42
  }, (0, _platformFeatureFlags.fg)('platform_synced_block_dogfooding') && {
43
43
  sourceSubType: ((_newResult$data3 = newResult.data) === null || _newResult$data3 === void 0 ? void 0 : _newResult$data3.sourceSubType) || ((_oldResult$data3 = oldResult.data) === null || _oldResult$data3 === void 0 ? void 0 : _oldResult$data3.sourceSubType) || undefined,
44
- onSamePage: ((_newResult$data4 = newResult.data) === null || _newResult$data4 === void 0 ? void 0 : _newResult$data4.onSamePage) || ((_oldResult$data4 = oldResult.data) === null || _oldResult$data4 === void 0 ? void 0 : _oldResult$data4.onSamePage) || undefined
44
+ onSameDocument: ((_newResult$data4 = newResult.data) === null || _newResult$data4 === void 0 ? void 0 : _newResult$data4.onSameDocument) || ((_oldResult$data4 = oldResult.data) === null || _oldResult$data4 === void 0 ? void 0 : _oldResult$data4.onSameDocument) || undefined
45
45
  })
46
46
  });
47
47
  };
@@ -0,0 +1,125 @@
1
+ import { createClient } from 'graphql-ws';
2
+ const GRAPHQL_WS_ENDPOINT = '/gateway/api/graphql/subscriptions';
3
+ let blockServiceClient = null;
4
+ const getBlockServiceClient = () => {
5
+ // Don't create client during SSR
6
+ if (typeof window === 'undefined') {
7
+ return null;
8
+ }
9
+ if (!blockServiceClient) {
10
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
11
+ const wsUrl = `${protocol}//${window.location.host}${GRAPHQL_WS_ENDPOINT}`;
12
+ blockServiceClient = createClient({
13
+ url: wsUrl,
14
+ lazy: true,
15
+ retryAttempts: 3
16
+ });
17
+ }
18
+ return blockServiceClient;
19
+ };
20
+ const SUBSCRIPTION_QUERY = `
21
+ subscription EDITOR_SYNCED_BLOCK_ON_BLOCK_UPDATED($resourceId: ID!) {
22
+ blockService_onBlockUpdated(resourceId: $resourceId) {
23
+ blockAri
24
+ blockInstanceId
25
+ content
26
+ contentUpdatedAt
27
+ createdAt
28
+ createdBy
29
+ deletionReason
30
+ product
31
+ sourceAri
32
+ status
33
+ }
34
+ }
35
+ `;
36
+ /**
37
+ * Extracts the resourceId from a block ARI.
38
+ * Block ARI format: ari:cloud:blocks:<cloudId>:synced-block/<resourceId>
39
+ * @param blockAri - The block ARI string
40
+ * @returns The resourceId portion of the ARI
41
+ */
42
+ const extractResourceIdFromBlockAri = blockAri => {
43
+ // eslint-disable-next-line require-unicode-regexp
44
+ const match = blockAri.match(/ari:cloud:blocks:[^:]+:synced-block\/(.+)$/);
45
+ return (match === null || match === void 0 ? void 0 : match[1]) || null;
46
+ };
47
+
48
+ /**
49
+ * Parses the subscription payload into a standardized format.
50
+ * @param payload - The raw subscription payload
51
+ * @returns Parsed block data or null if parsing fails
52
+ */
53
+ const parseSubscriptionPayload = payload => {
54
+ try {
55
+ const resourceId = extractResourceIdFromBlockAri(payload.blockAri);
56
+ if (!resourceId) {
57
+ return null;
58
+ }
59
+ let createdAt;
60
+ if (payload.createdAt !== undefined && payload.createdAt !== null) {
61
+ try {
62
+ // BE returns microseconds, convert to milliseconds
63
+ createdAt = new Date(payload.createdAt / 1000).toISOString();
64
+ } catch {
65
+ createdAt = undefined;
66
+ }
67
+ }
68
+ return {
69
+ blockAri: payload.blockAri,
70
+ blockInstanceId: payload.blockInstanceId,
71
+ content: JSON.parse(payload.content),
72
+ createdAt,
73
+ createdBy: payload.createdBy,
74
+ product: payload.product,
75
+ resourceId,
76
+ sourceAri: payload.sourceAri,
77
+ status: payload.status
78
+ };
79
+ } catch {
80
+ return null;
81
+ }
82
+ };
83
+
84
+ /**
85
+ * Creates a GraphQL subscription to block updates using the shared graphql-ws client.
86
+ *
87
+ * @param blockAri - The full block ARI to subscribe to (ari:cloud:blocks:{cloudId}:synced-block/{resourceId})
88
+ * @param onData - Callback function invoked when block data is updated
89
+ * @param onError - Optional callback function invoked on subscription errors
90
+ * @returns Unsubscribe function to close the subscription
91
+ */
92
+ export const subscribeToBlockUpdates = (blockAri, onData, onError) => {
93
+ const client = getBlockServiceClient();
94
+ if (!client) {
95
+ // Return a no-op unsubscribe if client is not available (e.g., SSR)
96
+ return () => {};
97
+ }
98
+ const unsubscribe = client.subscribe({
99
+ query: SUBSCRIPTION_QUERY,
100
+ variables: {
101
+ resourceId: blockAri
102
+ },
103
+ operationName: 'EDITOR_SYNCED_BLOCK_ON_BLOCK_UPDATED'
104
+ }, {
105
+ next: value => {
106
+ var _value$data;
107
+ if ((_value$data = value.data) !== null && _value$data !== void 0 && _value$data.blockService_onBlockUpdated) {
108
+ const parsed = parseSubscriptionPayload(value.data.blockService_onBlockUpdated);
109
+ if (parsed !== null) {
110
+ onData(parsed);
111
+ } else {
112
+ onError === null || onError === void 0 ? void 0 : onError(new Error('Failed to parse block subscription payload'));
113
+ }
114
+ }
115
+ },
116
+ error: error => {
117
+ const errorMessage = error instanceof Error ? error.message : 'GraphQL subscription error';
118
+ onError === null || onError === void 0 ? void 0 : onError(new Error(errorMessage));
119
+ },
120
+ complete: () => {
121
+ // Subscription completed
122
+ }
123
+ });
124
+ return unsubscribe;
125
+ };
@@ -0,0 +1,87 @@
1
+ /* eslint-disable require-unicode-regexp */
2
+
3
+ import { fetchWithRetry } from '../../utils/retry';
4
+ const COMMON_HEADERS = {
5
+ 'Content-Type': 'application/json',
6
+ Accept: 'application/json'
7
+ };
8
+ const AGG_HEADERS = {
9
+ 'X-ExperimentalApi': 'confluence-agg-beta'
10
+ };
11
+ const GRAPHQL_ENDPOINT = '/gateway/api/graphql';
12
+ const GET_SOURCE_INFO_OPERATION_NAME = 'EDITOR_SYNCED_BLOCK_GET_SOURCE_INFO';
13
+ /**
14
+ * Query to get the work item url by id
15
+ * @param id - the ID of the work item
16
+ * @returns url of the work item
17
+ */
18
+ const GET_SOURCE_INFO_QUERY = `query ${GET_SOURCE_INFO_OPERATION_NAME} ($id: ID!) {
19
+ jira {
20
+ issueById(id: $id) {
21
+ id
22
+ webUrl
23
+ summary
24
+ }
25
+ }}`;
26
+ const getJiraWorkItemSourceInfo = async ari => {
27
+ const bodyData = {
28
+ query: GET_SOURCE_INFO_QUERY,
29
+ operationName: GET_SOURCE_INFO_OPERATION_NAME,
30
+ variables: {
31
+ id: ari
32
+ }
33
+ };
34
+ const response = await fetchWithRetry(GRAPHQL_ENDPOINT, {
35
+ method: 'POST',
36
+ headers: {
37
+ ...COMMON_HEADERS,
38
+ ...AGG_HEADERS
39
+ },
40
+ body: JSON.stringify(bodyData)
41
+ });
42
+ if (!response.ok) {
43
+ throw new Error(`Failed to get url: ${response.statusText}`);
44
+ }
45
+ return await response.json();
46
+ };
47
+ const resolveNoAccessWorkItemInfo = async ari => {
48
+ const response = await fetch('/gateway/api/object-resolver/resolve/ari', {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ Accept: 'application/json'
53
+ },
54
+ body: JSON.stringify({
55
+ ari
56
+ })
57
+ });
58
+ if (response.ok) {
59
+ var _payload$data, _payload$data2;
60
+ const payload = await response.json();
61
+ const url = payload === null || payload === void 0 ? void 0 : (_payload$data = payload.data) === null || _payload$data === void 0 ? void 0 : _payload$data.url;
62
+ const title = payload === null || payload === void 0 ? void 0 : (_payload$data2 = payload.data) === null || _payload$data2 === void 0 ? void 0 : _payload$data2.name;
63
+ return {
64
+ url: typeof url === 'string' ? url : undefined,
65
+ title: typeof title === 'string' ? title : undefined,
66
+ sourceAri: ari
67
+ };
68
+ } else {
69
+ throw new Error(`Failed to resolve ari: ${response.statusText}`);
70
+ }
71
+ };
72
+ export const fetchJiraWorkItemInfo = async (workItemAri, hasAccess) => {
73
+ if (hasAccess) {
74
+ var _response$data, _response$data$jira;
75
+ const response = await getJiraWorkItemSourceInfo(workItemAri);
76
+ const contentData = (_response$data = response.data) === null || _response$data === void 0 ? void 0 : (_response$data$jira = _response$data.jira) === null || _response$data$jira === void 0 ? void 0 : _response$data$jira.issueById;
77
+ const webUrl = contentData === null || contentData === void 0 ? void 0 : contentData.webUrl;
78
+ const summary = contentData === null || contentData === void 0 ? void 0 : contentData.summary;
79
+ return Promise.resolve({
80
+ url: webUrl,
81
+ sourceAri: workItemAri,
82
+ title: summary
83
+ });
84
+ } else {
85
+ return await resolveNoAccessWorkItemInfo(workItemAri);
86
+ }
87
+ };
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
3
3
  import { fg } from '@atlaskit/platform-feature-flags';
4
4
  import { generateBlockAri, generateBlockAriFromReference } from '../../clients/block-service/ari';
5
5
  import { batchRetrieveSyncedBlocks, BlockError, createSyncedBlock, deleteSyncedBlock, getReferenceSyncedBlocks, getReferenceSyncedBlocksByBlockAri, getSyncedBlockContent, updateReferenceSyncedBlockOnDocument, updateSyncedBlock } from '../../clients/block-service/blockService';
6
+ import { subscribeToBlockUpdates as subscribeToBlockUpdatesWS } from '../../clients/block-service/blockSubscription';
6
7
  import { SyncBlockError } from '../../common/types';
7
8
  import { stringifyError } from '../../utils/errorHandling';
8
9
  import { createResourceIdForReference } from '../../utils/resourceId';
@@ -100,7 +101,8 @@ export const convertToSyncBlockData = (data, resourceId) => {
100
101
  createdBy: data.createdBy,
101
102
  product: data.product,
102
103
  resourceId,
103
- sourceAri: data.sourceAri
104
+ sourceAri: data.sourceAri,
105
+ status: data.status
104
106
  };
105
107
  };
106
108
  export const fetchReferences = async documentAri => {
@@ -186,7 +188,8 @@ class BlockServiceADFFetchProvider {
186
188
  blockInstanceId: blockContentResponse.blockInstanceId,
187
189
  // this was the node's localId, but has become the resourceId.
188
190
  sourceAri: blockContentResponse.sourceAri,
189
- product: blockContentResponse.product
191
+ product: blockContentResponse.product,
192
+ status: blockContentResponse.status
190
193
  },
191
194
  resourceId
192
195
  };
@@ -217,7 +220,7 @@ class BlockServiceADFFetchProvider {
217
220
  references.push({
218
221
  ...reference,
219
222
  hasAccess: true,
220
- onSamePage: this.parentAri === reference.documentAri
223
+ onSameDocument: this.parentAri === reference.documentAri
221
224
  });
222
225
  });
223
226
  response.errors.forEach(reference => {
@@ -226,7 +229,7 @@ class BlockServiceADFFetchProvider {
226
229
  blockAri: reference.blockAri,
227
230
  documentAri: reference.documentAri,
228
231
  hasAccess: false,
229
- onSamePage: false
232
+ onSameDocument: false
230
233
  });
231
234
  }
232
235
  });
@@ -311,7 +314,8 @@ class BlockServiceADFFetchProvider {
311
314
  resourceId: blockContentResponse.blockAri,
312
315
  blockInstanceId: blockContentResponse.blockInstanceId,
313
316
  sourceAri: blockContentResponse.sourceAri,
314
- product: blockContentResponse.product
317
+ product: blockContentResponse.product,
318
+ status: blockContentResponse.status
315
319
  },
316
320
  resourceId
317
321
  });
@@ -358,6 +362,37 @@ class BlockServiceADFFetchProvider {
358
362
  }));
359
363
  }
360
364
  }
365
+
366
+ /**
367
+ * Subscribes to real-time updates for a specific block using GraphQL WebSocket subscriptions.
368
+ * @param resourceId - The resource ID of the block to subscribe to
369
+ * @param onUpdate - Callback function invoked when the block is updated
370
+ * @param onError - Optional callback function invoked on subscription errors
371
+ * @returns Unsubscribe function to stop receiving updates
372
+ */
373
+ subscribeToBlockUpdates(resourceId, onUpdate, onError) {
374
+ const blockAri = generateBlockAriFromReference({
375
+ cloudId: this.cloudId,
376
+ resourceId
377
+ });
378
+ return subscribeToBlockUpdatesWS(blockAri, parsedData => {
379
+ // Convert ParsedBlockSubscriptionData to SyncBlockInstance
380
+ const syncBlockInstance = {
381
+ data: {
382
+ content: parsedData.content,
383
+ resourceId: parsedData.blockAri,
384
+ blockInstanceId: parsedData.blockInstanceId,
385
+ sourceAri: parsedData.sourceAri,
386
+ product: parsedData.product,
387
+ createdAt: parsedData.createdAt,
388
+ createdBy: parsedData.createdBy,
389
+ status: parsedData.status
390
+ },
391
+ resourceId: parsedData.resourceId
392
+ };
393
+ onUpdate(syncBlockInstance);
394
+ }, onError);
395
+ }
361
396
  }
362
397
  /**
363
398
  * ADFWriteProvider implementation that writes synced block data to Block Service API
@@ -4,6 +4,7 @@ import { fg } from '@atlaskit/platform-feature-flags';
4
4
  import { getProductFromSourceAri } from '../clients/block-service/ari';
5
5
  import { getPageIdAndTypeFromConfluencePageAri } from '../clients/confluence/ari';
6
6
  import { fetchConfluencePageInfo } from '../clients/confluence/sourceInfo';
7
+ import { fetchJiraWorkItemInfo } from '../clients/jira/sourceInfo';
7
8
  import { SyncBlockError } from '../common/types';
8
9
  import { SyncBlockDataProvider } from './types';
9
10
  export class SyncBlockProvider extends SyncBlockDataProvider {
@@ -204,7 +205,7 @@ export class SyncBlockProvider extends SyncBlockDataProvider {
204
205
  }
205
206
  return {
206
207
  ...sourceInfo,
207
- onSamePage: ((_this$writeProvider3 = this.writeProvider) === null || _this$writeProvider3 === void 0 ? void 0 : _this$writeProvider3.parentAri) === ari,
208
+ onSameDocument: ((_this$writeProvider3 = this.writeProvider) === null || _this$writeProvider3 === void 0 ? void 0 : _this$writeProvider3.parentAri) === ari,
208
209
  productType: product
209
210
  };
210
211
  } else {
@@ -213,7 +214,16 @@ export class SyncBlockProvider extends SyncBlockDataProvider {
213
214
  }
214
215
  case 'jira-work-item':
215
216
  if (fg('platform_synced_block_dogfooding')) {
216
- return Promise.resolve(undefined);
217
+ var _this$writeProvider4;
218
+ const sourceInfo = await fetchJiraWorkItemInfo(ari, hasAccess);
219
+ if (!sourceInfo) {
220
+ return Promise.resolve(undefined);
221
+ }
222
+ return {
223
+ ...sourceInfo,
224
+ onSameDocument: ((_this$writeProvider4 = this.writeProvider) === null || _this$writeProvider4 === void 0 ? void 0 : _this$writeProvider4.parentAri) === ari,
225
+ productType: product
226
+ };
217
227
  }
218
228
  return Promise.reject(new Error('Jira work item source product not supported'));
219
229
  default:
@@ -282,6 +292,20 @@ export class SyncBlockProvider extends SyncBlockDataProvider {
282
292
  }
283
293
  return this.fetchProvider.fetchReferences(isSource ? this.generateResourceIdForReference(resourceId) : resourceId);
284
294
  }
295
+
296
+ /**
297
+ * Subscribes to real-time updates for a specific block.
298
+ * @param resourceId - The resource ID of the block to subscribe to
299
+ * @param onUpdate - Callback function invoked when the block is updated
300
+ * @param onError - Optional callback function invoked on subscription errors
301
+ * @returns Unsubscribe function to stop receiving updates, or undefined if not supported
302
+ */
303
+ subscribeToBlockUpdates(resourceId, onUpdate, onError) {
304
+ if (this.fetchProvider.subscribeToBlockUpdates) {
305
+ return this.fetchProvider.subscribeToBlockUpdates(resourceId, onUpdate, onError);
306
+ }
307
+ return undefined;
308
+ }
285
309
  }
286
310
  const createSyncedBlockProvider = ({
287
311
  fetchProvider,