@atlaskit/editor-synced-block-provider 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @atlaskit/editor-synced-block-provider
2
2
 
3
+ ## 3.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`b7db97837674e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/b7db97837674e) -
8
+ Use GraphQL endpoint for fetching references on a document instead of Rest API
9
+
10
+ ## 3.1.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [`324fe88e2a5e0`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/324fe88e2a5e0) -
15
+ EDITOR-4048 Stop flickering on repositioning of synced blocks
16
+
3
17
  ## 3.0.0
4
18
 
5
19
  ### Patch Changes
@@ -20,11 +20,10 @@ var isBlockContentResponse = exports.isBlockContentResponse = function isBlockCo
20
20
  var content = response.content;
21
21
  return typeof content === 'string';
22
22
  };
23
-
24
23
  /**
25
24
  * Retrieves all synced blocks referenced in a document.
26
25
  *
27
- * Calls the Block Service API endpoint: `/v1/block/document/reference/{documentAri}`
26
+ * Calls the Block Service GraphQL API: `blockService_getDocumentReferenceBlocks`
28
27
  *
29
28
  * @param documentAri - The ARI of the document to fetch synced blocks for
30
29
  * @returns A promise containing arrays of successfully fetched blocks and any errors encountered
@@ -61,32 +60,51 @@ var isBlockContentResponse = exports.isBlockContentResponse = function isBlockCo
61
60
  * ]
62
61
  * }
63
62
  * ```
64
- * Check https://block-service.dev.atl-paas.net/ for latest API documentation.
65
63
  */
66
64
  var getReferenceSyncedBlocks = exports.getReferenceSyncedBlocks = /*#__PURE__*/function () {
67
65
  var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(documentAri) {
68
- var response;
66
+ var bodyData, response, result;
69
67
  return _regenerator.default.wrap(function _callee$(_context) {
70
68
  while (1) switch (_context.prev = _context.next) {
71
69
  case 0:
72
- _context.next = 2;
73
- return (0, _retry.fetchWithRetry)("".concat(BLOCK_SERVICE_API_URL, "/block/document/reference/").concat(encodeURIComponent(documentAri)), {
74
- method: 'GET',
75
- headers: COMMON_HEADERS
70
+ bodyData = {
71
+ query: buildGetDocumentReferenceBlocksQuery(documentAri),
72
+ operationName: GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME
73
+ };
74
+ _context.next = 3;
75
+ return (0, _retry.fetchWithRetry)(GRAPHQL_ENDPOINT, {
76
+ method: 'POST',
77
+ headers: COMMON_HEADERS,
78
+ body: JSON.stringify(bodyData)
76
79
  });
77
- case 2:
80
+ case 3:
78
81
  response = _context.sent;
79
82
  if (response.ok) {
80
- _context.next = 5;
83
+ _context.next = 6;
81
84
  break;
82
85
  }
83
86
  throw new BlockError(response.status);
84
- case 5:
85
- _context.next = 7;
87
+ case 6:
88
+ _context.next = 8;
86
89
  return response.json();
87
- case 7:
88
- return _context.abrupt("return", _context.sent);
89
90
  case 8:
91
+ result = _context.sent;
92
+ if (!(result.errors && result.errors.length > 0)) {
93
+ _context.next = 11;
94
+ break;
95
+ }
96
+ throw new Error(result.errors.map(function (e) {
97
+ return e.message;
98
+ }).join(', '));
99
+ case 11:
100
+ if (result.data) {
101
+ _context.next = 13;
102
+ break;
103
+ }
104
+ throw new Error('No data returned from GraphQL query');
105
+ case 13:
106
+ return _context.abrupt("return", result.data.blockService_getDocumentReferenceBlocks);
107
+ case 14:
90
108
  case "end":
91
109
  return _context.stop();
92
110
  }
@@ -101,6 +119,11 @@ var COMMON_HEADERS = {
101
119
  Accept: 'application/json'
102
120
  };
103
121
  var BLOCK_SERVICE_API_URL = '/gateway/api/blocks/v1';
122
+ var GRAPHQL_ENDPOINT = '/gateway/api/graphql';
123
+ var GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME = 'EDITOR_SYNCED_BLOCK_GET_DOCUMENT_REFERENCE_BLOCKS';
124
+ var buildGetDocumentReferenceBlocksQuery = function buildGetDocumentReferenceBlocksQuery(documentAri) {
125
+ return "query ".concat(GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME, " {\n\tblockService_getDocumentReferenceBlocks(documentAri: \"").concat(documentAri, "\") {\n\t\tblocks {\n\t\t\tblockAri\n\t\t\tblockInstanceId\n\t\t\tcontent\n\t\t\tcreatedAt\n\t\t\tcreatedBy\n\t\t\tproduct\n\t\t\tsourceAri\n\t\t\tstatus\n\t\t\tversion\n\t\t}\n\t\terrors {\n\t\t\tblockAri\n\t\t\tcode\n\t\t\treason\n\t\t}\n\t}\n}");
126
+ };
104
127
  var BlockError = exports.BlockError = /*#__PURE__*/function (_Error) {
105
128
  function BlockError(status) {
106
129
  var _this;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.useFetchSyncBlockData = void 0;
8
8
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
10
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
11
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
11
12
  var _react = require("react");
@@ -13,21 +14,32 @@ var _monitoring = require("@atlaskit/editor-common/monitoring");
13
14
  var _types = require("../common/types");
14
15
  var _errorHandling = require("../utils/errorHandling");
15
16
  var _utils = require("../utils/utils");
17
+ 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; }
18
+ 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; }
16
19
  var useFetchSyncBlockData = exports.useFetchSyncBlockData = function useFetchSyncBlockData(manager, resourceId, localId, fireAnalyticsEvent) {
20
+ // Initialize both states from a single cache lookup to avoid race conditions.
21
+ // When a block is moved/remounted, the old component's cleanup may clear the cache
22
+ // before or after the new component mounts. By doing a single lookup, we ensure
23
+ // consistency between syncBlockInstance and isLoading initial values.
17
24
  var _useState = (0, _react.useState)(function () {
18
25
  if (resourceId) {
19
- var _manager$referenceMan, _manager$referenceMan2;
20
- return (_manager$referenceMan = manager === null || manager === void 0 || (_manager$referenceMan2 = manager.referenceManager) === null || _manager$referenceMan2 === void 0 ? void 0 : _manager$referenceMan2.getInitialSyncBlockData(resourceId)) !== null && _manager$referenceMan !== void 0 ? _manager$referenceMan : null;
26
+ var _manager$referenceMan;
27
+ var initialData = manager === null || manager === void 0 || (_manager$referenceMan = manager.referenceManager) === null || _manager$referenceMan === void 0 ? void 0 : _manager$referenceMan.getInitialSyncBlockData(resourceId);
28
+ return {
29
+ syncBlockInstance: initialData !== null && initialData !== void 0 ? initialData : null,
30
+ isLoading: initialData === undefined
31
+ };
21
32
  }
22
- return null;
33
+ return {
34
+ syncBlockInstance: null,
35
+ isLoading: true
36
+ };
23
37
  }),
24
38
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
25
- syncBlockInstance = _useState2[0],
26
- setSyncBlockInstance = _useState2[1];
27
- var _useState3 = (0, _react.useState)(true),
28
- _useState4 = (0, _slicedToArray2.default)(_useState3, 2),
29
- isLoading = _useState4[0],
30
- setIsLoading = _useState4[1];
39
+ _useState2$ = _useState2[0],
40
+ syncBlockInstance = _useState2$.syncBlockInstance,
41
+ isLoading = _useState2$.isLoading,
42
+ setFetchState = _useState2[1];
31
43
  var reloadData = (0, _react.useCallback)( /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
32
44
  var syncBlockNode;
33
45
  return _regenerator.default.wrap(function _callee$(_context) {
@@ -47,13 +59,17 @@ var useFetchSyncBlockData = exports.useFetchSyncBlockData = function useFetchSyn
47
59
  }
48
60
  throw new Error('Failed to create sync block node from resourceid and localid');
49
61
  case 6:
50
- setIsLoading(true);
62
+ setFetchState(function (prev) {
63
+ return _objectSpread(_objectSpread({}, prev), {}, {
64
+ isLoading: true
65
+ });
66
+ });
51
67
 
52
68
  // Fetch sync block data, the `subscribeToSyncBlock` will update the state once data is fetched
53
69
  _context.next = 9;
54
70
  return manager.referenceManager.fetchSyncBlocksData([syncBlockNode]);
55
71
  case 9:
56
- _context.next = 16;
72
+ _context.next = 17;
57
73
  break;
58
74
  case 11:
59
75
  _context.prev = 11;
@@ -64,13 +80,21 @@ var useFetchSyncBlockData = exports.useFetchSyncBlockData = function useFetchSyn
64
80
  fireAnalyticsEvent === null || fireAnalyticsEvent === void 0 || fireAnalyticsEvent((0, _errorHandling.fetchErrorPayload)(_context.t0.message));
65
81
 
66
82
  // Set error state if fetching fails
67
- setSyncBlockInstance({
68
- resourceId: resourceId || '',
69
- error: _types.SyncBlockError.Errored
83
+ setFetchState({
84
+ syncBlockInstance: {
85
+ resourceId: resourceId || '',
86
+ error: _types.SyncBlockError.Errored
87
+ },
88
+ isLoading: false
70
89
  });
71
- case 16:
72
- setIsLoading(false);
90
+ return _context.abrupt("return");
73
91
  case 17:
92
+ setFetchState(function (prev) {
93
+ return _objectSpread(_objectSpread({}, prev), {}, {
94
+ isLoading: false
95
+ });
96
+ });
97
+ case 18:
74
98
  case "end":
75
99
  return _context.stop();
76
100
  }
@@ -78,8 +102,10 @@ var useFetchSyncBlockData = exports.useFetchSyncBlockData = function useFetchSyn
78
102
  })), [isLoading, localId, manager.referenceManager, resourceId, fireAnalyticsEvent]);
79
103
  (0, _react.useEffect)(function () {
80
104
  var unsubscribe = manager.referenceManager.subscribeToSyncBlock(resourceId || '', localId || '', function (data) {
81
- setSyncBlockInstance(data);
82
- setIsLoading(false);
105
+ setFetchState({
106
+ syncBlockInstance: data,
107
+ isLoading: false
108
+ });
83
109
  });
84
110
  return function () {
85
111
  unsubscribe();
@@ -28,6 +28,11 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
28
28
  // Handles fetching source URL and title for sync blocks.
29
29
  // Can be used in both editor and renderer contexts.
30
30
  var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
31
+ // Track pending cache deletions to handle block moves (unmount/remount)
32
+ // When a block is moved, the old component unmounts before the new one mounts,
33
+ // causing the cache to be deleted prematurely. We delay deletion to allow
34
+ // the new component to subscribe and cancel the pending deletion.
35
+
31
36
  function ReferenceSyncBlockStoreManager(dataProvider) {
32
37
  (0, _classCallCheck2.default)(this, ReferenceSyncBlockStoreManager);
33
38
  // Keeps track of addition and deletion of reference synced blocks on the document
@@ -42,6 +47,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
42
47
  this.syncBlockFetchDataRequests = new Map();
43
48
  this.syncBlockSourceInfoRequests = new Map();
44
49
  this.providerFactories = new Map();
50
+ this.pendingCacheDeletions = new Map();
45
51
  }
46
52
  return (0, _createClass2.default)(ReferenceSyncBlockStoreManager, [{
47
53
  key: "setFireAnalyticsEvent",
@@ -341,6 +347,16 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
341
347
  value: function subscribeToSyncBlock(resourceId, localId, callback) {
342
348
  var _this$dataProvider2,
343
349
  _this3 = this;
350
+ // Cancel any pending cache deletion for this resourceId.
351
+ // This handles the case where a block is moved - the old component unmounts
352
+ // (scheduling deletion) but the new component mounts and subscribes before
353
+ // the deletion timeout fires.
354
+ var pendingDeletion = this.pendingCacheDeletions.get(resourceId);
355
+ if (pendingDeletion) {
356
+ clearTimeout(pendingDeletion);
357
+ this.pendingCacheDeletions.delete(resourceId);
358
+ }
359
+
344
360
  // add to subscriptions map
345
361
  var resourceSubscriptions = this.subscriptions.get(resourceId) || {};
346
362
  this.subscriptions.set(resourceId, _objectSpread(_objectSpread({}, resourceSubscriptions), {}, (0, _defineProperty2.default)({}, localId, callback)));
@@ -371,7 +387,19 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
371
387
  delete resourceSubscriptions[localId];
372
388
  if (Object.keys(resourceSubscriptions).length === 0) {
373
389
  _this3.subscriptions.delete(resourceId);
374
- _this3.deleteFromCache(resourceId);
390
+ // Delay cache deletion to handle block moves (unmount/remount).
391
+ // When a block is moved, the old component unmounts before the new one mounts.
392
+ // By delaying deletion, we give the new component time to subscribe and
393
+ // cancel this pending deletion, preserving the cached data.
394
+ // TODO: EDITOR-4152 - Rework this logic
395
+ var deletionTimeout = setTimeout(function () {
396
+ // Only delete if still no subscribers (wasn't re-subscribed)
397
+ if (!_this3.subscriptions.has(resourceId)) {
398
+ _this3.deleteFromCache(resourceId);
399
+ }
400
+ _this3.pendingCacheDeletions.delete(resourceId);
401
+ }, 1000);
402
+ _this3.pendingCacheDeletions.set(resourceId, deletionTimeout);
375
403
  } else {
376
404
  _this3.subscriptions.set(resourceId, resourceSubscriptions);
377
405
  }
@@ -3,11 +3,10 @@ export const isBlockContentResponse = response => {
3
3
  const content = response.content;
4
4
  return typeof content === 'string';
5
5
  };
6
-
7
6
  /**
8
7
  * Retrieves all synced blocks referenced in a document.
9
8
  *
10
- * Calls the Block Service API endpoint: `/v1/block/document/reference/{documentAri}`
9
+ * Calls the Block Service GraphQL API: `blockService_getDocumentReferenceBlocks`
11
10
  *
12
11
  * @param documentAri - The ARI of the document to fetch synced blocks for
13
12
  * @returns A promise containing arrays of successfully fetched blocks and any errors encountered
@@ -44,23 +43,56 @@ export const isBlockContentResponse = response => {
44
43
  * ]
45
44
  * }
46
45
  * ```
47
- * Check https://block-service.dev.atl-paas.net/ for latest API documentation.
48
46
  */
49
47
  export const getReferenceSyncedBlocks = async documentAri => {
50
- const response = await fetchWithRetry(`${BLOCK_SERVICE_API_URL}/block/document/reference/${encodeURIComponent(documentAri)}`, {
51
- method: 'GET',
52
- headers: COMMON_HEADERS
48
+ const bodyData = {
49
+ query: buildGetDocumentReferenceBlocksQuery(documentAri),
50
+ operationName: GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME
51
+ };
52
+ const response = await fetchWithRetry(GRAPHQL_ENDPOINT, {
53
+ method: 'POST',
54
+ headers: COMMON_HEADERS,
55
+ body: JSON.stringify(bodyData)
53
56
  });
54
57
  if (!response.ok) {
55
58
  throw new BlockError(response.status);
56
59
  }
57
- return await response.json();
60
+ const result = await response.json();
61
+ if (result.errors && result.errors.length > 0) {
62
+ throw new Error(result.errors.map(e => e.message).join(', '));
63
+ }
64
+ if (!result.data) {
65
+ throw new Error('No data returned from GraphQL query');
66
+ }
67
+ return result.data.blockService_getDocumentReferenceBlocks;
58
68
  };
59
69
  const COMMON_HEADERS = {
60
70
  'Content-Type': 'application/json',
61
71
  Accept: 'application/json'
62
72
  };
63
73
  const BLOCK_SERVICE_API_URL = '/gateway/api/blocks/v1';
74
+ const GRAPHQL_ENDPOINT = '/gateway/api/graphql';
75
+ const GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME = 'EDITOR_SYNCED_BLOCK_GET_DOCUMENT_REFERENCE_BLOCKS';
76
+ const buildGetDocumentReferenceBlocksQuery = documentAri => `query ${GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME} {
77
+ blockService_getDocumentReferenceBlocks(documentAri: "${documentAri}") {
78
+ blocks {
79
+ blockAri
80
+ blockInstanceId
81
+ content
82
+ createdAt
83
+ createdBy
84
+ product
85
+ sourceAri
86
+ status
87
+ version
88
+ }
89
+ errors {
90
+ blockAri
91
+ code
92
+ reason
93
+ }
94
+ }
95
+ }`;
64
96
  export class BlockError extends Error {
65
97
  constructor(status) {
66
98
  super(`Block error`);
@@ -4,14 +4,27 @@ import { SyncBlockError } from '../common/types';
4
4
  import { fetchErrorPayload } from '../utils/errorHandling';
5
5
  import { createSyncBlockNode } from '../utils/utils';
6
6
  export const useFetchSyncBlockData = (manager, resourceId, localId, fireAnalyticsEvent) => {
7
- const [syncBlockInstance, setSyncBlockInstance] = useState(() => {
7
+ // Initialize both states from a single cache lookup to avoid race conditions.
8
+ // When a block is moved/remounted, the old component's cleanup may clear the cache
9
+ // before or after the new component mounts. By doing a single lookup, we ensure
10
+ // consistency between syncBlockInstance and isLoading initial values.
11
+ const [{
12
+ syncBlockInstance,
13
+ isLoading
14
+ }, setFetchState] = useState(() => {
8
15
  if (resourceId) {
9
- var _manager$referenceMan, _manager$referenceMan2;
10
- return (_manager$referenceMan = manager === null || manager === void 0 ? void 0 : (_manager$referenceMan2 = manager.referenceManager) === null || _manager$referenceMan2 === void 0 ? void 0 : _manager$referenceMan2.getInitialSyncBlockData(resourceId)) !== null && _manager$referenceMan !== void 0 ? _manager$referenceMan : null;
16
+ var _manager$referenceMan;
17
+ const initialData = manager === null || manager === void 0 ? void 0 : (_manager$referenceMan = manager.referenceManager) === null || _manager$referenceMan === void 0 ? void 0 : _manager$referenceMan.getInitialSyncBlockData(resourceId);
18
+ return {
19
+ syncBlockInstance: initialData !== null && initialData !== void 0 ? initialData : null,
20
+ isLoading: initialData === undefined
21
+ };
11
22
  }
12
- return null;
23
+ return {
24
+ syncBlockInstance: null,
25
+ isLoading: true
26
+ };
13
27
  });
14
- const [isLoading, setIsLoading] = useState(true);
15
28
  const reloadData = useCallback(async () => {
16
29
  if (isLoading) {
17
30
  return;
@@ -21,7 +34,10 @@ export const useFetchSyncBlockData = (manager, resourceId, localId, fireAnalytic
21
34
  if (!syncBlockNode) {
22
35
  throw new Error('Failed to create sync block node from resourceid and localid');
23
36
  }
24
- setIsLoading(true);
37
+ setFetchState(prev => ({
38
+ ...prev,
39
+ isLoading: true
40
+ }));
25
41
 
26
42
  // Fetch sync block data, the `subscribeToSyncBlock` will update the state once data is fetched
27
43
  await manager.referenceManager.fetchSyncBlocksData([syncBlockNode]);
@@ -32,17 +48,26 @@ export const useFetchSyncBlockData = (manager, resourceId, localId, fireAnalytic
32
48
  fireAnalyticsEvent === null || fireAnalyticsEvent === void 0 ? void 0 : fireAnalyticsEvent(fetchErrorPayload(error.message));
33
49
 
34
50
  // Set error state if fetching fails
35
- setSyncBlockInstance({
36
- resourceId: resourceId || '',
37
- error: SyncBlockError.Errored
51
+ setFetchState({
52
+ syncBlockInstance: {
53
+ resourceId: resourceId || '',
54
+ error: SyncBlockError.Errored
55
+ },
56
+ isLoading: false
38
57
  });
58
+ return;
39
59
  }
40
- setIsLoading(false);
60
+ setFetchState(prev => ({
61
+ ...prev,
62
+ isLoading: false
63
+ }));
41
64
  }, [isLoading, localId, manager.referenceManager, resourceId, fireAnalyticsEvent]);
42
65
  useEffect(() => {
43
66
  const unsubscribe = manager.referenceManager.subscribeToSyncBlock(resourceId || '', localId || '', data => {
44
- setSyncBlockInstance(data);
45
- setIsLoading(false);
67
+ setFetchState({
68
+ syncBlockInstance: data,
69
+ isLoading: false
70
+ });
46
71
  });
47
72
  return () => {
48
73
  unsubscribe();
@@ -12,6 +12,11 @@ import { createSyncBlockNode } from '../utils/utils';
12
12
  // Handles fetching source URL and title for sync blocks.
13
13
  // Can be used in both editor and renderer contexts.
14
14
  export class ReferenceSyncBlockStoreManager {
15
+ // Track pending cache deletions to handle block moves (unmount/remount)
16
+ // When a block is moved, the old component unmounts before the new one mounts,
17
+ // causing the cache to be deleted prematurely. We delay deletion to allow
18
+ // the new component to subscribe and cancel the pending deletion.
19
+
15
20
  constructor(dataProvider) {
16
21
  // Keeps track of addition and deletion of reference synced blocks on the document
17
22
  // This starts as true to always flush the cache when document is saved for the first time
@@ -25,6 +30,7 @@ export class ReferenceSyncBlockStoreManager {
25
30
  this.syncBlockFetchDataRequests = new Map();
26
31
  this.syncBlockSourceInfoRequests = new Map();
27
32
  this.providerFactories = new Map();
33
+ this.pendingCacheDeletions = new Map();
28
34
  }
29
35
  setFireAnalyticsEvent(fireAnalyticsEvent) {
30
36
  this.fireAnalyticsEvent = fireAnalyticsEvent;
@@ -220,6 +226,16 @@ export class ReferenceSyncBlockStoreManager {
220
226
  }
221
227
  subscribeToSyncBlock(resourceId, localId, callback) {
222
228
  var _this$dataProvider2, _this$dataProvider2$g;
229
+ // Cancel any pending cache deletion for this resourceId.
230
+ // This handles the case where a block is moved - the old component unmounts
231
+ // (scheduling deletion) but the new component mounts and subscribes before
232
+ // the deletion timeout fires.
233
+ const pendingDeletion = this.pendingCacheDeletions.get(resourceId);
234
+ if (pendingDeletion) {
235
+ clearTimeout(pendingDeletion);
236
+ this.pendingCacheDeletions.delete(resourceId);
237
+ }
238
+
223
239
  // add to subscriptions map
224
240
  const resourceSubscriptions = this.subscriptions.get(resourceId) || {};
225
241
  this.subscriptions.set(resourceId, {
@@ -253,7 +269,19 @@ export class ReferenceSyncBlockStoreManager {
253
269
  delete resourceSubscriptions[localId];
254
270
  if (Object.keys(resourceSubscriptions).length === 0) {
255
271
  this.subscriptions.delete(resourceId);
256
- this.deleteFromCache(resourceId);
272
+ // Delay cache deletion to handle block moves (unmount/remount).
273
+ // When a block is moved, the old component unmounts before the new one mounts.
274
+ // By delaying deletion, we give the new component time to subscribe and
275
+ // cancel this pending deletion, preserving the cached data.
276
+ // TODO: EDITOR-4152 - Rework this logic
277
+ const deletionTimeout = setTimeout(() => {
278
+ // Only delete if still no subscribers (wasn't re-subscribed)
279
+ if (!this.subscriptions.has(resourceId)) {
280
+ this.deleteFromCache(resourceId);
281
+ }
282
+ this.pendingCacheDeletions.delete(resourceId);
283
+ }, 1000);
284
+ this.pendingCacheDeletions.set(resourceId, deletionTimeout);
257
285
  } else {
258
286
  this.subscriptions.set(resourceId, resourceSubscriptions);
259
287
  }
@@ -13,11 +13,10 @@ export var isBlockContentResponse = function isBlockContentResponse(response) {
13
13
  var content = response.content;
14
14
  return typeof content === 'string';
15
15
  };
16
-
17
16
  /**
18
17
  * Retrieves all synced blocks referenced in a document.
19
18
  *
20
- * Calls the Block Service API endpoint: `/v1/block/document/reference/{documentAri}`
19
+ * Calls the Block Service GraphQL API: `blockService_getDocumentReferenceBlocks`
21
20
  *
22
21
  * @param documentAri - The ARI of the document to fetch synced blocks for
23
22
  * @returns A promise containing arrays of successfully fetched blocks and any errors encountered
@@ -54,32 +53,51 @@ export var isBlockContentResponse = function isBlockContentResponse(response) {
54
53
  * ]
55
54
  * }
56
55
  * ```
57
- * Check https://block-service.dev.atl-paas.net/ for latest API documentation.
58
56
  */
59
57
  export var getReferenceSyncedBlocks = /*#__PURE__*/function () {
60
58
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(documentAri) {
61
- var response;
59
+ var bodyData, response, result;
62
60
  return _regeneratorRuntime.wrap(function _callee$(_context) {
63
61
  while (1) switch (_context.prev = _context.next) {
64
62
  case 0:
65
- _context.next = 2;
66
- return fetchWithRetry("".concat(BLOCK_SERVICE_API_URL, "/block/document/reference/").concat(encodeURIComponent(documentAri)), {
67
- method: 'GET',
68
- headers: COMMON_HEADERS
63
+ bodyData = {
64
+ query: buildGetDocumentReferenceBlocksQuery(documentAri),
65
+ operationName: GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME
66
+ };
67
+ _context.next = 3;
68
+ return fetchWithRetry(GRAPHQL_ENDPOINT, {
69
+ method: 'POST',
70
+ headers: COMMON_HEADERS,
71
+ body: JSON.stringify(bodyData)
69
72
  });
70
- case 2:
73
+ case 3:
71
74
  response = _context.sent;
72
75
  if (response.ok) {
73
- _context.next = 5;
76
+ _context.next = 6;
74
77
  break;
75
78
  }
76
79
  throw new BlockError(response.status);
77
- case 5:
78
- _context.next = 7;
80
+ case 6:
81
+ _context.next = 8;
79
82
  return response.json();
80
- case 7:
81
- return _context.abrupt("return", _context.sent);
82
83
  case 8:
84
+ result = _context.sent;
85
+ if (!(result.errors && result.errors.length > 0)) {
86
+ _context.next = 11;
87
+ break;
88
+ }
89
+ throw new Error(result.errors.map(function (e) {
90
+ return e.message;
91
+ }).join(', '));
92
+ case 11:
93
+ if (result.data) {
94
+ _context.next = 13;
95
+ break;
96
+ }
97
+ throw new Error('No data returned from GraphQL query');
98
+ case 13:
99
+ return _context.abrupt("return", result.data.blockService_getDocumentReferenceBlocks);
100
+ case 14:
83
101
  case "end":
84
102
  return _context.stop();
85
103
  }
@@ -94,6 +112,11 @@ var COMMON_HEADERS = {
94
112
  Accept: 'application/json'
95
113
  };
96
114
  var BLOCK_SERVICE_API_URL = '/gateway/api/blocks/v1';
115
+ var GRAPHQL_ENDPOINT = '/gateway/api/graphql';
116
+ var GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME = 'EDITOR_SYNCED_BLOCK_GET_DOCUMENT_REFERENCE_BLOCKS';
117
+ var buildGetDocumentReferenceBlocksQuery = function buildGetDocumentReferenceBlocksQuery(documentAri) {
118
+ return "query ".concat(GET_DOCUMENT_REFERENCE_BLOCKS_OPERATION_NAME, " {\n\tblockService_getDocumentReferenceBlocks(documentAri: \"").concat(documentAri, "\") {\n\t\tblocks {\n\t\t\tblockAri\n\t\t\tblockInstanceId\n\t\t\tcontent\n\t\t\tcreatedAt\n\t\t\tcreatedBy\n\t\t\tproduct\n\t\t\tsourceAri\n\t\t\tstatus\n\t\t\tversion\n\t\t}\n\t\terrors {\n\t\t\tblockAri\n\t\t\tcode\n\t\t\treason\n\t\t}\n\t}\n}");
119
+ };
97
120
  export var BlockError = /*#__PURE__*/function (_Error) {
98
121
  function BlockError(status) {
99
122
  var _this;
@@ -1,26 +1,38 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
1
2
  import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
4
  import _regeneratorRuntime from "@babel/runtime/regenerator";
5
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
6
+ 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
7
  import { useCallback, useEffect, useState } from 'react';
5
8
  import { logException } from '@atlaskit/editor-common/monitoring';
6
9
  import { SyncBlockError } from '../common/types';
7
10
  import { fetchErrorPayload } from '../utils/errorHandling';
8
11
  import { createSyncBlockNode } from '../utils/utils';
9
12
  export var useFetchSyncBlockData = function useFetchSyncBlockData(manager, resourceId, localId, fireAnalyticsEvent) {
13
+ // Initialize both states from a single cache lookup to avoid race conditions.
14
+ // When a block is moved/remounted, the old component's cleanup may clear the cache
15
+ // before or after the new component mounts. By doing a single lookup, we ensure
16
+ // consistency between syncBlockInstance and isLoading initial values.
10
17
  var _useState = useState(function () {
11
18
  if (resourceId) {
12
- var _manager$referenceMan, _manager$referenceMan2;
13
- return (_manager$referenceMan = manager === null || manager === void 0 || (_manager$referenceMan2 = manager.referenceManager) === null || _manager$referenceMan2 === void 0 ? void 0 : _manager$referenceMan2.getInitialSyncBlockData(resourceId)) !== null && _manager$referenceMan !== void 0 ? _manager$referenceMan : null;
19
+ var _manager$referenceMan;
20
+ var initialData = manager === null || manager === void 0 || (_manager$referenceMan = manager.referenceManager) === null || _manager$referenceMan === void 0 ? void 0 : _manager$referenceMan.getInitialSyncBlockData(resourceId);
21
+ return {
22
+ syncBlockInstance: initialData !== null && initialData !== void 0 ? initialData : null,
23
+ isLoading: initialData === undefined
24
+ };
14
25
  }
15
- return null;
26
+ return {
27
+ syncBlockInstance: null,
28
+ isLoading: true
29
+ };
16
30
  }),
17
31
  _useState2 = _slicedToArray(_useState, 2),
18
- syncBlockInstance = _useState2[0],
19
- setSyncBlockInstance = _useState2[1];
20
- var _useState3 = useState(true),
21
- _useState4 = _slicedToArray(_useState3, 2),
22
- isLoading = _useState4[0],
23
- setIsLoading = _useState4[1];
32
+ _useState2$ = _useState2[0],
33
+ syncBlockInstance = _useState2$.syncBlockInstance,
34
+ isLoading = _useState2$.isLoading,
35
+ setFetchState = _useState2[1];
24
36
  var reloadData = useCallback( /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
25
37
  var syncBlockNode;
26
38
  return _regeneratorRuntime.wrap(function _callee$(_context) {
@@ -40,13 +52,17 @@ export var useFetchSyncBlockData = function useFetchSyncBlockData(manager, resou
40
52
  }
41
53
  throw new Error('Failed to create sync block node from resourceid and localid');
42
54
  case 6:
43
- setIsLoading(true);
55
+ setFetchState(function (prev) {
56
+ return _objectSpread(_objectSpread({}, prev), {}, {
57
+ isLoading: true
58
+ });
59
+ });
44
60
 
45
61
  // Fetch sync block data, the `subscribeToSyncBlock` will update the state once data is fetched
46
62
  _context.next = 9;
47
63
  return manager.referenceManager.fetchSyncBlocksData([syncBlockNode]);
48
64
  case 9:
49
- _context.next = 16;
65
+ _context.next = 17;
50
66
  break;
51
67
  case 11:
52
68
  _context.prev = 11;
@@ -57,13 +73,21 @@ export var useFetchSyncBlockData = function useFetchSyncBlockData(manager, resou
57
73
  fireAnalyticsEvent === null || fireAnalyticsEvent === void 0 || fireAnalyticsEvent(fetchErrorPayload(_context.t0.message));
58
74
 
59
75
  // Set error state if fetching fails
60
- setSyncBlockInstance({
61
- resourceId: resourceId || '',
62
- error: SyncBlockError.Errored
76
+ setFetchState({
77
+ syncBlockInstance: {
78
+ resourceId: resourceId || '',
79
+ error: SyncBlockError.Errored
80
+ },
81
+ isLoading: false
63
82
  });
64
- case 16:
65
- setIsLoading(false);
83
+ return _context.abrupt("return");
66
84
  case 17:
85
+ setFetchState(function (prev) {
86
+ return _objectSpread(_objectSpread({}, prev), {}, {
87
+ isLoading: false
88
+ });
89
+ });
90
+ case 18:
67
91
  case "end":
68
92
  return _context.stop();
69
93
  }
@@ -71,8 +95,10 @@ export var useFetchSyncBlockData = function useFetchSyncBlockData(manager, resou
71
95
  })), [isLoading, localId, manager.referenceManager, resourceId, fireAnalyticsEvent]);
72
96
  useEffect(function () {
73
97
  var unsubscribe = manager.referenceManager.subscribeToSyncBlock(resourceId || '', localId || '', function (data) {
74
- setSyncBlockInstance(data);
75
- setIsLoading(false);
98
+ setFetchState({
99
+ syncBlockInstance: data,
100
+ isLoading: false
101
+ });
76
102
  });
77
103
  return function () {
78
104
  unsubscribe();
@@ -22,6 +22,11 @@ import { createSyncBlockNode } from '../utils/utils';
22
22
  // Handles fetching source URL and title for sync blocks.
23
23
  // Can be used in both editor and renderer contexts.
24
24
  export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
25
+ // Track pending cache deletions to handle block moves (unmount/remount)
26
+ // When a block is moved, the old component unmounts before the new one mounts,
27
+ // causing the cache to be deleted prematurely. We delay deletion to allow
28
+ // the new component to subscribe and cancel the pending deletion.
29
+
25
30
  function ReferenceSyncBlockStoreManager(dataProvider) {
26
31
  _classCallCheck(this, ReferenceSyncBlockStoreManager);
27
32
  // Keeps track of addition and deletion of reference synced blocks on the document
@@ -36,6 +41,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
36
41
  this.syncBlockFetchDataRequests = new Map();
37
42
  this.syncBlockSourceInfoRequests = new Map();
38
43
  this.providerFactories = new Map();
44
+ this.pendingCacheDeletions = new Map();
39
45
  }
40
46
  return _createClass(ReferenceSyncBlockStoreManager, [{
41
47
  key: "setFireAnalyticsEvent",
@@ -335,6 +341,16 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
335
341
  value: function subscribeToSyncBlock(resourceId, localId, callback) {
336
342
  var _this$dataProvider2,
337
343
  _this3 = this;
344
+ // Cancel any pending cache deletion for this resourceId.
345
+ // This handles the case where a block is moved - the old component unmounts
346
+ // (scheduling deletion) but the new component mounts and subscribes before
347
+ // the deletion timeout fires.
348
+ var pendingDeletion = this.pendingCacheDeletions.get(resourceId);
349
+ if (pendingDeletion) {
350
+ clearTimeout(pendingDeletion);
351
+ this.pendingCacheDeletions.delete(resourceId);
352
+ }
353
+
338
354
  // add to subscriptions map
339
355
  var resourceSubscriptions = this.subscriptions.get(resourceId) || {};
340
356
  this.subscriptions.set(resourceId, _objectSpread(_objectSpread({}, resourceSubscriptions), {}, _defineProperty({}, localId, callback)));
@@ -365,7 +381,19 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
365
381
  delete resourceSubscriptions[localId];
366
382
  if (Object.keys(resourceSubscriptions).length === 0) {
367
383
  _this3.subscriptions.delete(resourceId);
368
- _this3.deleteFromCache(resourceId);
384
+ // Delay cache deletion to handle block moves (unmount/remount).
385
+ // When a block is moved, the old component unmounts before the new one mounts.
386
+ // By delaying deletion, we give the new component time to subscribe and
387
+ // cancel this pending deletion, preserving the cached data.
388
+ // TODO: EDITOR-4152 - Rework this logic
389
+ var deletionTimeout = setTimeout(function () {
390
+ // Only delete if still no subscribers (wasn't re-subscribed)
391
+ if (!_this3.subscriptions.has(resourceId)) {
392
+ _this3.deleteFromCache(resourceId);
393
+ }
394
+ _this3.pendingCacheDeletions.delete(resourceId);
395
+ }, 1000);
396
+ _this3.pendingCacheDeletions.set(resourceId, deletionTimeout);
369
397
  } else {
370
398
  _this3.subscriptions.set(resourceId, resourceSubscriptions);
371
399
  }
@@ -23,7 +23,7 @@ export declare const isBlockContentResponse: (response: BlockContentResponse | B
23
23
  /**
24
24
  * Retrieves all synced blocks referenced in a document.
25
25
  *
26
- * Calls the Block Service API endpoint: `/v1/block/document/reference/{documentAri}`
26
+ * Calls the Block Service GraphQL API: `blockService_getDocumentReferenceBlocks`
27
27
  *
28
28
  * @param documentAri - The ARI of the document to fetch synced blocks for
29
29
  * @returns A promise containing arrays of successfully fetched blocks and any errors encountered
@@ -60,7 +60,6 @@ export declare const isBlockContentResponse: (response: BlockContentResponse | B
60
60
  * ]
61
61
  * }
62
62
  * ```
63
- * Check https://block-service.dev.atl-paas.net/ for latest API documentation.
64
63
  */
65
64
  export declare const getReferenceSyncedBlocks: (documentAri: string) => Promise<ReferenceSyncedBlockResponse>;
66
65
  export type GetSyncedBlockContentRequest = {
@@ -14,6 +14,7 @@ export declare class ReferenceSyncBlockStoreManager {
14
14
  private syncBlockFetchDataRequests;
15
15
  private syncBlockSourceInfoRequests;
16
16
  private isRefreshingSubscriptions;
17
+ private pendingCacheDeletions;
17
18
  constructor(dataProvider?: SyncBlockDataProvider);
18
19
  setFireAnalyticsEvent(fireAnalyticsEvent?: (payload: RendererSyncBlockEventPayload) => void): void;
19
20
  generateResourceIdForReference(sourceId: ResourceId): ResourceId;
@@ -23,7 +23,7 @@ export declare const isBlockContentResponse: (response: BlockContentResponse | B
23
23
  /**
24
24
  * Retrieves all synced blocks referenced in a document.
25
25
  *
26
- * Calls the Block Service API endpoint: `/v1/block/document/reference/{documentAri}`
26
+ * Calls the Block Service GraphQL API: `blockService_getDocumentReferenceBlocks`
27
27
  *
28
28
  * @param documentAri - The ARI of the document to fetch synced blocks for
29
29
  * @returns A promise containing arrays of successfully fetched blocks and any errors encountered
@@ -60,7 +60,6 @@ export declare const isBlockContentResponse: (response: BlockContentResponse | B
60
60
  * ]
61
61
  * }
62
62
  * ```
63
- * Check https://block-service.dev.atl-paas.net/ for latest API documentation.
64
63
  */
65
64
  export declare const getReferenceSyncedBlocks: (documentAri: string) => Promise<ReferenceSyncedBlockResponse>;
66
65
  export type GetSyncedBlockContentRequest = {
@@ -14,6 +14,7 @@ export declare class ReferenceSyncBlockStoreManager {
14
14
  private syncBlockFetchDataRequests;
15
15
  private syncBlockSourceInfoRequests;
16
16
  private isRefreshingSubscriptions;
17
+ private pendingCacheDeletions;
17
18
  constructor(dataProvider?: SyncBlockDataProvider);
18
19
  setFireAnalyticsEvent(fireAnalyticsEvent?: (payload: RendererSyncBlockEventPayload) => void): void;
19
20
  generateResourceIdForReference(sourceId: ResourceId): ResourceId;
package/package.json CHANGED
@@ -76,7 +76,7 @@
76
76
  }
77
77
  },
78
78
  "name": "@atlaskit/editor-synced-block-provider",
79
- "version": "3.0.0",
79
+ "version": "3.2.0",
80
80
  "description": "Synced Block Provider for @atlaskit/editor-plugin-synced-block",
81
81
  "author": "Atlassian Pty Ltd",
82
82
  "license": "Apache-2.0",