@atlaskit/editor-plugin-synced-block 0.1.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 (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE.md +11 -0
  3. package/README.md +1 -0
  4. package/SyncedBlock/package.json +15 -0
  5. package/afm-cc/tsconfig.json +44 -0
  6. package/build/tsconfig.json +22 -0
  7. package/dist/cjs/index.js +12 -0
  8. package/dist/cjs/pm-plugins/SyncClient.js +167 -0
  9. package/dist/cjs/pm-plugins/main.js +50 -0
  10. package/dist/cjs/pm-plugins/utils.js +19 -0
  11. package/dist/cjs/syncedBlockPlugin.js +24 -0
  12. package/dist/cjs/syncedBlockPluginType.js +5 -0
  13. package/dist/cjs/types/index.js +1 -0
  14. package/dist/cjs/ui/extensions/synced-block/components/GlobalStyles.js +25 -0
  15. package/dist/cjs/ui/extensions/synced-block/components/SyncedBlockLiveView.js +25 -0
  16. package/dist/cjs/ui/extensions/synced-block/components/SyncedBlockRenderer.js +25 -0
  17. package/dist/cjs/ui/extensions/synced-block/constants.js +32 -0
  18. package/dist/cjs/ui/extensions/synced-block/getSyncedBlockExtensionProvider.js +11 -0
  19. package/dist/cjs/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.js +29 -0
  20. package/dist/cjs/ui/extensions/synced-block/hooks/usePollContentProperty.js +121 -0
  21. package/dist/cjs/ui/extensions/synced-block/index.js +19 -0
  22. package/dist/cjs/ui/extensions/synced-block/manifest.js +254 -0
  23. package/dist/cjs/ui/extensions/synced-block/utils/ari.js +29 -0
  24. package/dist/cjs/ui/extensions/synced-block/utils/content-property.js +159 -0
  25. package/dist/cjs/ui/extensions/synced-block/utils/synced-block.js +65 -0
  26. package/dist/es2019/index.js +4 -0
  27. package/dist/es2019/pm-plugins/SyncClient.js +102 -0
  28. package/dist/es2019/pm-plugins/main.js +47 -0
  29. package/dist/es2019/pm-plugins/utils.js +13 -0
  30. package/dist/es2019/syncedBlockPlugin.js +17 -0
  31. package/dist/es2019/syncedBlockPluginType.js +1 -0
  32. package/dist/es2019/types/index.js +0 -0
  33. package/dist/es2019/ui/extensions/synced-block/components/GlobalStyles.js +18 -0
  34. package/dist/es2019/ui/extensions/synced-block/components/SyncedBlockLiveView.js +19 -0
  35. package/dist/es2019/ui/extensions/synced-block/components/SyncedBlockRenderer.js +19 -0
  36. package/dist/es2019/ui/extensions/synced-block/constants.js +26 -0
  37. package/dist/es2019/ui/extensions/synced-block/getSyncedBlockExtensionProvider.js +5 -0
  38. package/dist/es2019/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.js +24 -0
  39. package/dist/es2019/ui/extensions/synced-block/hooks/usePollContentProperty.js +107 -0
  40. package/dist/es2019/ui/extensions/synced-block/index.js +5 -0
  41. package/dist/es2019/ui/extensions/synced-block/manifest.js +147 -0
  42. package/dist/es2019/ui/extensions/synced-block/utils/ari.js +19 -0
  43. package/dist/es2019/ui/extensions/synced-block/utils/content-property.js +108 -0
  44. package/dist/es2019/ui/extensions/synced-block/utils/synced-block.js +57 -0
  45. package/dist/esm/index.js +4 -0
  46. package/dist/esm/pm-plugins/SyncClient.js +160 -0
  47. package/dist/esm/pm-plugins/main.js +44 -0
  48. package/dist/esm/pm-plugins/utils.js +13 -0
  49. package/dist/esm/syncedBlockPlugin.js +17 -0
  50. package/dist/esm/syncedBlockPluginType.js +1 -0
  51. package/dist/esm/types/index.js +0 -0
  52. package/dist/esm/ui/extensions/synced-block/components/GlobalStyles.js +18 -0
  53. package/dist/esm/ui/extensions/synced-block/components/SyncedBlockLiveView.js +18 -0
  54. package/dist/esm/ui/extensions/synced-block/components/SyncedBlockRenderer.js +18 -0
  55. package/dist/esm/ui/extensions/synced-block/constants.js +26 -0
  56. package/dist/esm/ui/extensions/synced-block/getSyncedBlockExtensionProvider.js +5 -0
  57. package/dist/esm/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.js +23 -0
  58. package/dist/esm/ui/extensions/synced-block/hooks/usePollContentProperty.js +114 -0
  59. package/dist/esm/ui/extensions/synced-block/index.js +5 -0
  60. package/dist/esm/ui/extensions/synced-block/manifest.js +247 -0
  61. package/dist/esm/ui/extensions/synced-block/utils/ari.js +23 -0
  62. package/dist/esm/ui/extensions/synced-block/utils/content-property.js +153 -0
  63. package/dist/esm/ui/extensions/synced-block/utils/synced-block.js +58 -0
  64. package/dist/types/index.d.ts +3 -0
  65. package/dist/types/pm-plugins/SyncClient.d.ts +14 -0
  66. package/dist/types/pm-plugins/main.d.ts +6 -0
  67. package/dist/types/pm-plugins/utils.d.ts +5 -0
  68. package/dist/types/syncedBlockPlugin.d.ts +2 -0
  69. package/dist/types/syncedBlockPluginType.d.ts +2 -0
  70. package/dist/types/types/index.d.ts +3 -0
  71. package/dist/types/ui/extensions/synced-block/components/GlobalStyles.d.ts +6 -0
  72. package/dist/types/ui/extensions/synced-block/components/SyncedBlockLiveView.d.ts +7 -0
  73. package/dist/types/ui/extensions/synced-block/components/SyncedBlockRenderer.d.ts +7 -0
  74. package/dist/types/ui/extensions/synced-block/constants.d.ts +8 -0
  75. package/dist/types/ui/extensions/synced-block/getSyncedBlockExtensionProvider.d.ts +2 -0
  76. package/dist/types/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.d.ts +6 -0
  77. package/dist/types/ui/extensions/synced-block/hooks/usePollContentProperty.d.ts +7 -0
  78. package/dist/types/ui/extensions/synced-block/index.d.ts +2 -0
  79. package/dist/types/ui/extensions/synced-block/manifest.d.ts +2 -0
  80. package/dist/types/ui/extensions/synced-block/utils/ari.d.ts +4 -0
  81. package/dist/types/ui/extensions/synced-block/utils/content-property.d.ts +33 -0
  82. package/dist/types/ui/extensions/synced-block/utils/synced-block.d.ts +24 -0
  83. package/dist/types-ts4.5/index.d.ts +3 -0
  84. package/dist/types-ts4.5/pm-plugins/SyncClient.d.ts +14 -0
  85. package/dist/types-ts4.5/pm-plugins/main.d.ts +6 -0
  86. package/dist/types-ts4.5/pm-plugins/utils.d.ts +5 -0
  87. package/dist/types-ts4.5/syncedBlockPlugin.d.ts +2 -0
  88. package/dist/types-ts4.5/syncedBlockPluginType.d.ts +2 -0
  89. package/dist/types-ts4.5/types/index.d.ts +3 -0
  90. package/dist/types-ts4.5/ui/extensions/synced-block/components/GlobalStyles.d.ts +6 -0
  91. package/dist/types-ts4.5/ui/extensions/synced-block/components/SyncedBlockLiveView.d.ts +7 -0
  92. package/dist/types-ts4.5/ui/extensions/synced-block/components/SyncedBlockRenderer.d.ts +7 -0
  93. package/dist/types-ts4.5/ui/extensions/synced-block/constants.d.ts +8 -0
  94. package/dist/types-ts4.5/ui/extensions/synced-block/getSyncedBlockExtensionProvider.d.ts +2 -0
  95. package/dist/types-ts4.5/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.d.ts +6 -0
  96. package/dist/types-ts4.5/ui/extensions/synced-block/hooks/usePollContentProperty.d.ts +7 -0
  97. package/dist/types-ts4.5/ui/extensions/synced-block/index.d.ts +2 -0
  98. package/dist/types-ts4.5/ui/extensions/synced-block/manifest.d.ts +2 -0
  99. package/dist/types-ts4.5/ui/extensions/synced-block/utils/ari.d.ts +4 -0
  100. package/dist/types-ts4.5/ui/extensions/synced-block/utils/content-property.d.ts +33 -0
  101. package/dist/types-ts4.5/ui/extensions/synced-block/utils/synced-block.d.ts +24 -0
  102. package/docs/0-intro.tsx +43 -0
  103. package/package.json +88 -0
  104. package/tsconfig.json +8 -0
@@ -0,0 +1,102 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { JSONTransformer } from '@atlaskit/editor-json-transformer';
3
+ import { getContentPropertyIdFromAri, getPageIdFromAri } from '../ui/extensions/synced-block/utils/ari';
4
+ import { getContentProperty, updateContentProperty } from '../ui/extensions/synced-block/utils/content-property';
5
+ import { parseSyncedBlockContentPropertyValue, stringifySyncedBlockContentPropertyValue } from '../ui/extensions/synced-block/utils/synced-block';
6
+ const transformer = new JSONTransformer();
7
+ const toJSON = node => transformer.encodeNode(node);
8
+ const getCacheKey = ({
9
+ sourceDocumentAri,
10
+ contentAri,
11
+ contentPropertyKey
12
+ }) => `${sourceDocumentAri}-${contentAri}-${contentPropertyKey}`;
13
+ export class SyncClient {
14
+ constructor() {
15
+ _defineProperty(this, "requestMap", new Map());
16
+ this.requestMap = new Map();
17
+ }
18
+ getRequestState(key) {
19
+ return this.requestMap.get(key);
20
+ }
21
+ setRequestState(key, state) {
22
+ this.requestMap.set(key, state);
23
+ }
24
+ async sendRequest({
25
+ sourceDocumentAri,
26
+ contentAri,
27
+ contentPropertyKey,
28
+ value
29
+ }) {
30
+ const pageId = getPageIdFromAri(sourceDocumentAri);
31
+ const contentPropertyId = getContentPropertyIdFromAri(contentAri);
32
+ try {
33
+ const contentProperty = await getContentProperty({
34
+ pageId,
35
+ contentPropertyId
36
+ });
37
+ const updatedValue = stringifySyncedBlockContentPropertyValue({
38
+ ...parseSyncedBlockContentPropertyValue(contentProperty.value),
39
+ ...JSON.parse(value)
40
+ });
41
+ await updateContentProperty({
42
+ pageId,
43
+ key: contentPropertyKey,
44
+ value: updatedValue,
45
+ signal: undefined
46
+ });
47
+ } catch (error) {
48
+ // eslint-disable-next-line no-console
49
+ console.error('Failed to update content property:', error);
50
+ }
51
+ }
52
+ syncContent({
53
+ sourceDocumentAri,
54
+ contentAri,
55
+ contentPropertyKey,
56
+ node
57
+ }) {
58
+ const nodeAdf = toJSON(node);
59
+ const key = getCacheKey({
60
+ sourceDocumentAri,
61
+ contentAri,
62
+ contentPropertyKey
63
+ });
64
+ const value = stringifySyncedBlockContentPropertyValue({
65
+ adf: nodeAdf
66
+ });
67
+ const requestState = this.getRequestState(key) || {
68
+ timeout: null,
69
+ pendingValue: null,
70
+ isSending: false
71
+ };
72
+ requestState.pendingValue = value;
73
+ if (requestState.isSending) {
74
+ return;
75
+ }
76
+ if (requestState.timeout) {
77
+ clearTimeout(requestState.timeout);
78
+ }
79
+ const send = async () => {
80
+ if (requestState.isSending) {
81
+ return;
82
+ }
83
+ requestState.isSending = true;
84
+ try {
85
+ await this.sendRequest({
86
+ sourceDocumentAri,
87
+ contentAri,
88
+ contentPropertyKey,
89
+ value: requestState.pendingValue || ''
90
+ });
91
+ requestState.pendingValue = null;
92
+ } catch (error) {
93
+ // eslint-disable-next-line no-console
94
+ console.error('Failed to send synced block content:', error);
95
+ } finally {
96
+ requestState.isSending = false;
97
+ }
98
+ };
99
+ requestState.timeout = setTimeout(send, 1000);
100
+ this.setRequestState(key, requestState);
101
+ }
102
+ }
@@ -0,0 +1,47 @@
1
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
3
+ import { SyncClient } from './SyncClient';
4
+ import { findSyncedBlockParent } from './utils';
5
+ export const syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
8
+
9
+ export const createPlugin = () => {
10
+ const syncClient = new SyncClient();
11
+ return new SafePlugin({
12
+ key: syncedBlockPluginKey,
13
+ state: {
14
+ init() {
15
+ return {};
16
+ },
17
+ apply: (tr, currentPluginState) => {
18
+ if (tr.docChanged) {
19
+ const $pos = tr.selection.$from;
20
+ const syncedBlockParent = findSyncedBlockParent($pos);
21
+ if (syncedBlockParent) {
22
+ const {
23
+ node,
24
+ attributes
25
+ } = syncedBlockParent;
26
+ const {
27
+ sourceDocumentAri,
28
+ contentAri,
29
+ contentPropertyKey
30
+ } = attributes.parameters;
31
+ syncClient.syncContent({
32
+ sourceDocumentAri,
33
+ contentAri,
34
+ contentPropertyKey,
35
+ node
36
+ });
37
+ }
38
+ }
39
+ const meta = tr.getMeta(syncedBlockPluginKey);
40
+ if (meta) {
41
+ return meta;
42
+ }
43
+ return currentPluginState;
44
+ }
45
+ }
46
+ });
47
+ };
@@ -0,0 +1,13 @@
1
+ import { isSyncedBlockAttributes } from '../ui/extensions/synced-block/utils/synced-block';
2
+ export const findSyncedBlockParent = $pos => {
3
+ for (let i = 0; i <= $pos.depth; i++) {
4
+ const node = $pos.node(i);
5
+ if (isSyncedBlockAttributes(node.attrs)) {
6
+ return {
7
+ node,
8
+ attributes: node.attrs
9
+ };
10
+ }
11
+ }
12
+ return;
13
+ };
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { createPlugin } from './pm-plugins/main';
3
+ import { GlobalStylesWrapper } from './ui/extensions/synced-block/components/GlobalStyles';
4
+ export const syncedBlockPlugin = () => {
5
+ return {
6
+ name: 'syncedBlock',
7
+ pmPlugins() {
8
+ return [{
9
+ name: 'syncedBlockPlugin',
10
+ plugin: createPlugin
11
+ }];
12
+ },
13
+ contentComponent() {
14
+ return /*#__PURE__*/React.createElement(GlobalStylesWrapper, null);
15
+ }
16
+ };
17
+ };
@@ -0,0 +1 @@
1
+ export {};
File without changes
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @atlaskit/ui-styling-standard/no-global-styles
6
+ import { css, Global, jsx } from '@emotion/react';
7
+ const extensionStyles = css({
8
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
9
+ '[extensionkey="synced-block:reference"] .ak-renderer-wrapper > div:last-of-type': {
10
+ padding: "var(--ds-space-250, 20px)",
11
+ paddingRight: "var(--ds-space-250, 20px)"
12
+ }
13
+ });
14
+ export const GlobalStylesWrapper = () => {
15
+ return jsx(Global, {
16
+ styles: [extensionStyles]
17
+ });
18
+ };
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { useLiveSyncedBlockContent } from '../hooks/useLiveSyncedBlockContent';
3
+ import SyncedBlockRenderer from './SyncedBlockRenderer';
4
+ const SyncedBlockLiveView = ({
5
+ sourceDocumentAri,
6
+ contentAri
7
+ }) => {
8
+ const syncedBlockContent = useLiveSyncedBlockContent({
9
+ sourceDocumentAri,
10
+ contentAri
11
+ });
12
+ if (!syncedBlockContent) {
13
+ return /*#__PURE__*/React.createElement("div", null, "Loading...");
14
+ }
15
+ return /*#__PURE__*/React.createElement(SyncedBlockRenderer, {
16
+ syncedBlockContent: syncedBlockContent
17
+ });
18
+ };
19
+ export default SyncedBlockLiveView;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { ReactRenderer } from '@atlaskit/renderer';
3
+ import { RendererActionsContext } from '@atlaskit/renderer/actions';
4
+ const SyncedBlockRenderer = ({
5
+ syncedBlockContent
6
+ }) => {
7
+ return /*#__PURE__*/React.createElement(RendererActionsContext, null, /*#__PURE__*/React.createElement(ReactRenderer, {
8
+ adfStage: "stage0"
9
+ // @ts-ignore
10
+ ,
11
+ document: {
12
+ type: 'doc',
13
+ version: 1,
14
+ content: syncedBlockContent.adf.content
15
+ },
16
+ appearance: "full-page"
17
+ }));
18
+ };
19
+ export default SyncedBlockRenderer;
@@ -0,0 +1,26 @@
1
+ // hello.atlassian.net cloud id
2
+ const HELLO_CLOUD_ID = 'a436116f-02ce-4520-8fbb-7301462a1674';
3
+
4
+ // spike page https://hello.atlassian.net/wiki/spaces/~7120208ef57ce4d614485e876489301a16b906/pages/5626233808
5
+ const TEST_PAGE_ID = '5626233808';
6
+ export const getPageId = () => {
7
+ var _window$location$href, _window$location$href2, _window$location$path;
8
+ return (
9
+ // eslint-disable-next-line require-unicode-regexp
10
+ ((_window$location$href = window.location.href.match(/pageId=(\d+)/)) === null || _window$location$href === void 0 ? void 0 : _window$location$href[1]) || ( // eslint-disable-next-line require-unicode-regexp
11
+ (_window$location$href2 = window.location.href.match(/pages\/edit-v2\/(\d+)/)) === null || _window$location$href2 === void 0 ? void 0 : _window$location$href2[1]) || ( // eslint-disable-next-line require-unicode-regexp
12
+ (_window$location$path = window.location.pathname.match(/pages\/(\d+)/)) === null || _window$location$path === void 0 ? void 0 : _window$location$path[1]) ||
13
+ // view page or live doc
14
+ TEST_PAGE_ID
15
+ );
16
+ };
17
+ /**
18
+ * This by no means is a stable way to get the cloud id, but it works for now.
19
+ * We should switch passing the cloud id from Confluence to a Editor plugin,
20
+ * for instance the user preferences plugin would have a seperate place for user and cloud info
21
+ * @returns the cloud id from the initial state
22
+ */
23
+ export const getCloudId = () => {
24
+ var _INITIAL_STATE__, _INITIAL_STATE__$meta;
25
+ return ((_INITIAL_STATE__ = window.__INITIAL_STATE__) === null || _INITIAL_STATE__ === void 0 ? void 0 : (_INITIAL_STATE__$meta = _INITIAL_STATE__.meta) === null || _INITIAL_STATE__$meta === void 0 ? void 0 : _INITIAL_STATE__$meta['cloud-id']) || HELLO_CLOUD_ID;
26
+ };
@@ -0,0 +1,5 @@
1
+ import { DefaultExtensionProvider } from '@atlaskit/editor-common/extensions';
2
+ import { getSyncedBlockManifest } from './manifest';
3
+ export const getSyncedBlockExtensionProvider = () => {
4
+ return new DefaultExtensionProvider([getSyncedBlockManifest()]);
5
+ };
@@ -0,0 +1,24 @@
1
+ import { useMemo } from 'react';
2
+ import { usePollContentProperty } from '../hooks/usePollContentProperty';
3
+ import { parseSyncedBlockContentPropertyValue } from '../utils/synced-block';
4
+ export const useLiveSyncedBlockContent = ({
5
+ sourceDocumentAri,
6
+ contentAri
7
+ }) => {
8
+ const contentProperty = usePollContentProperty({
9
+ sourceDocumentAri,
10
+ contentAri
11
+ });
12
+ return useMemo(() => {
13
+ if (!contentProperty) {
14
+ return null;
15
+ }
16
+ try {
17
+ return parseSyncedBlockContentPropertyValue(contentProperty.value);
18
+ } catch (error) {
19
+ // eslint-disable-next-line no-console
20
+ console.error('Failed to extract synced block content:', error);
21
+ return null;
22
+ }
23
+ }, [contentProperty]);
24
+ };
@@ -0,0 +1,107 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { getContentPropertyIdFromAri, getPageIdFromAri } from '../utils/ari';
3
+ import { getContentProperty } from '../utils/content-property';
4
+ const POLLING_INTERVAL = 1000;
5
+ const cache = new Map();
6
+ const inFlightRequests = new Map();
7
+ const subscribers = new Map();
8
+ const pollingTimeouts = new Map();
9
+ const lastRequestTimes = new Map();
10
+ const getRequestKey = (pageId, contentPropertyId) => `${pageId}:${contentPropertyId}`;
11
+ const fetchContentPropertyWithDedup = (pageId, contentPropertyId) => {
12
+ const requestKey = getRequestKey(pageId, contentPropertyId);
13
+ lastRequestTimes.set(requestKey, Date.now());
14
+ const inFlightRequest = inFlightRequests.get(requestKey);
15
+ if (inFlightRequest) {
16
+ return inFlightRequest;
17
+ }
18
+ const requestPromise = getContentProperty({
19
+ pageId,
20
+ contentPropertyId
21
+ }).then(result => {
22
+ cache.set(requestKey, result);
23
+ const subscribersForKey = subscribers.get(requestKey);
24
+ if (subscribersForKey) {
25
+ subscribersForKey.forEach(callback => callback(result));
26
+ }
27
+ inFlightRequests.delete(requestKey);
28
+ if (subscribersForKey && subscribersForKey.size > 0) {
29
+ scheduleNextPoll(pageId, contentPropertyId);
30
+ }
31
+ return result;
32
+ }).catch(error => {
33
+ inFlightRequests.delete(requestKey);
34
+ const subscribersForKey = subscribers.get(requestKey);
35
+ if (subscribersForKey && subscribersForKey.size > 0) {
36
+ scheduleNextPoll(pageId, contentPropertyId);
37
+ }
38
+ throw error;
39
+ });
40
+ inFlightRequests.set(requestKey, requestPromise);
41
+ return requestPromise;
42
+ };
43
+ const scheduleNextPoll = (pageId, contentPropertyId) => {
44
+ const requestKey = getRequestKey(pageId, contentPropertyId);
45
+ const existingTimeout = pollingTimeouts.get(requestKey);
46
+ if (existingTimeout) {
47
+ clearTimeout(existingTimeout);
48
+ }
49
+ const lastRequestTime = lastRequestTimes.get(requestKey) || 0;
50
+ const timeElapsed = Date.now() - lastRequestTime;
51
+ const delay = Math.max(100, POLLING_INTERVAL - timeElapsed);
52
+ const timeout = setTimeout(() => {
53
+ const subscribersForKey = subscribers.get(requestKey);
54
+ if (subscribersForKey && subscribersForKey.size > 0) {
55
+ fetchContentPropertyWithDedup(pageId, contentPropertyId).catch(error => {
56
+ // eslint-disable-next-line no-console
57
+ console.error('Failed to fetch content property:', error);
58
+ });
59
+ } else {
60
+ pollingTimeouts.delete(requestKey);
61
+ }
62
+ }, delay);
63
+ pollingTimeouts.set(requestKey, timeout);
64
+ };
65
+ export const usePollContentProperty = ({
66
+ sourceDocumentAri,
67
+ contentAri
68
+ }) => {
69
+ const [contentProperty, setContentProperty] = useState();
70
+ const initializedRef = useRef(false);
71
+ useEffect(() => {
72
+ const pageId = getPageIdFromAri(sourceDocumentAri);
73
+ const contentPropertyId = getContentPropertyIdFromAri(contentAri);
74
+ const requestKey = getRequestKey(pageId, contentPropertyId);
75
+ const subscribersForKey = subscribers.get(requestKey) || new Set();
76
+ if (!subscribers.has(requestKey)) {
77
+ subscribers.set(requestKey, subscribersForKey);
78
+ }
79
+ subscribersForKey.add(setContentProperty);
80
+ const cachedValue = cache.get(requestKey);
81
+ if (cachedValue) {
82
+ setContentProperty(cachedValue);
83
+ }
84
+ if (subscribersForKey.size === 1 || !initializedRef.current) {
85
+ initializedRef.current = true;
86
+ fetchContentPropertyWithDedup(pageId, contentPropertyId).catch(error => {
87
+ // eslint-disable-next-line no-console
88
+ console.error('Failed to fetch content property:', error);
89
+ });
90
+ }
91
+ return () => {
92
+ subscribersForKey.delete(setContentProperty);
93
+ if (subscribersForKey.size === 0) {
94
+ subscribers.delete(requestKey);
95
+ const existingTimeout = pollingTimeouts.get(requestKey);
96
+ if (existingTimeout) {
97
+ clearTimeout(existingTimeout);
98
+ pollingTimeouts.delete(requestKey);
99
+ }
100
+ }
101
+ };
102
+ }, [sourceDocumentAri, contentAri]);
103
+ const pageId = getPageIdFromAri(sourceDocumentAri);
104
+ const contentPropertyId = getContentPropertyIdFromAri(contentAri);
105
+ const requestKey = getRequestKey(pageId, contentPropertyId);
106
+ return contentProperty || cache.get(requestKey);
107
+ };
@@ -0,0 +1,5 @@
1
+ // Disable no-re-export rule for entry point files
2
+ /* eslint-disable @atlaskit/editor/no-re-export */
3
+
4
+ export { getSyncedBlockExtensionProvider } from './getSyncedBlockExtensionProvider';
5
+ export { getSyncedBlockManifest } from './manifest';
@@ -0,0 +1,147 @@
1
+ import React from 'react';
2
+ import SmartLinkIcon from '@atlaskit/icon/core/smart-link';
3
+ import SyncedBlockLiveView from './components/SyncedBlockLiveView';
4
+ import { getPageId } from './constants';
5
+ import { getConfluencePageAri, getContentPropertyAri } from './utils/ari';
6
+ import { createContentProperty } from './utils/content-property';
7
+ import { SYNCED_BLOCK_EXTENSION_KEY, SYNCED_BLOCK_EXTENSION_TYPE, SYNCED_BLOCK_REFERENCE_KEY, SYNCED_BLOCK_REFERENCE_NODE, SYNCED_BLOCK_SOURCE_KEY, SYNCED_BLOCK_SOURCE_NODE, getDefaultSyncedBlockContent, isSyncedBlockAttributes, stringifySyncedBlockContentPropertyValue } from './utils/synced-block';
8
+ const getRandomId = () => {
9
+ if (!globalThis.crypto || typeof globalThis.crypto.randomUUID !== 'function') {
10
+ return new Date().toISOString();
11
+ }
12
+ return globalThis.crypto.randomUUID();
13
+ };
14
+
15
+ // Remaining tasks
16
+ // - Better location for content sync implementation – currently done in SyncedBlockSource renderer which won't work in editor
17
+ // - Could implement an editor plugin to do this, if there's no native way to do it with extensions
18
+ // - Implement separate content property for storage of the metadata of a synced block, separate from the content
19
+ // - Update polling to use the metadata content property, then if metadata updated, fetch the content property
20
+ // - Investigate re-rendering of SyncedBlockReference in editor on every document change (is this just atlaskit behavior?)
21
+ // - On copy of the synced block, transform into a reference
22
+ // - Move implementation into Confluence and test in branch environment
23
+ // - Dealing with orphaned synced block content properties data
24
+ // - Getting current page id in editor context and the cloud id
25
+ // - Explore hiding the frame for the extension in the editor, currently using `__hideFrame: true` and commented code to enable
26
+ export const getSyncedBlockManifest = () => ({
27
+ title: 'Synced Block',
28
+ type: SYNCED_BLOCK_EXTENSION_TYPE,
29
+ key: SYNCED_BLOCK_EXTENSION_KEY,
30
+ description: 'Synced block spike',
31
+ icons: {
32
+ // Ignored via go/ees005
33
+ // eslint-disable-next-line require-await
34
+ '48': async () => () => /*#__PURE__*/React.createElement(SmartLinkIcon, {
35
+ label: "Synced Block",
36
+ size: "medium"
37
+ })
38
+ },
39
+ modules: {
40
+ quickInsert: [{
41
+ key: 'quick-insert-synced-block-source',
42
+ action: async _api => {
43
+ const contentPropertyKey = `synced-block-` + getRandomId();
44
+ const content = getDefaultSyncedBlockContent();
45
+ const value = stringifySyncedBlockContentPropertyValue({
46
+ adf: content
47
+ });
48
+ const contentProperty = await createContentProperty({
49
+ pageId: getPageId(),
50
+ key: contentPropertyKey,
51
+ value
52
+ });
53
+ const attributes = {
54
+ extensionType: SYNCED_BLOCK_EXTENSION_TYPE,
55
+ extensionKey: SYNCED_BLOCK_SOURCE_KEY,
56
+ parameters: {
57
+ sourceDocumentAri: getConfluencePageAri(getPageId()),
58
+ contentAri: getContentPropertyAri(contentProperty.id),
59
+ contentPropertyKey
60
+ },
61
+ localId: 'testId'
62
+ };
63
+ content.attrs = attributes;
64
+ return content;
65
+ }
66
+ }],
67
+ nodes: {
68
+ [SYNCED_BLOCK_SOURCE_NODE]: {
69
+ type: 'bodiedExtension',
70
+ // Ignored via go/ees005
71
+ // eslint-disable-next-line require-await
72
+ render: async () => props => {
73
+ if (!isSyncedBlockAttributes(props.node)) {
74
+ return null;
75
+ }
76
+ const {
77
+ sourceDocumentAri,
78
+ contentAri
79
+ } = props.node.parameters;
80
+ return /*#__PURE__*/React.createElement(SyncedBlockLiveView, {
81
+ sourceDocumentAri: sourceDocumentAri,
82
+ contentAri: contentAri
83
+ });
84
+ },
85
+ // @ts-expect-error
86
+ __hideFrame: true
87
+ },
88
+ [SYNCED_BLOCK_REFERENCE_NODE]: {
89
+ type: 'extension',
90
+ // Ignored via go/ees005
91
+ // eslint-disable-next-line require-await
92
+ render: async () => props => {
93
+ if (!isSyncedBlockAttributes(props.node)) {
94
+ return null;
95
+ }
96
+ const {
97
+ sourceDocumentAri,
98
+ contentAri
99
+ } = props.node.parameters;
100
+ return /*#__PURE__*/React.createElement(SyncedBlockLiveView, {
101
+ sourceDocumentAri: sourceDocumentAri,
102
+ contentAri: contentAri
103
+ });
104
+ },
105
+ // @ts-expect-error
106
+ __hideFrame: true
107
+ }
108
+ },
109
+ contextualToolbars: [{
110
+ context: {
111
+ type: 'extension',
112
+ nodeType: 'bodiedExtension',
113
+ extensionType: SYNCED_BLOCK_EXTENSION_TYPE,
114
+ extensionKey: SYNCED_BLOCK_SOURCE_KEY
115
+ },
116
+ toolbarItems: [{
117
+ key: 'toolbar-item-key',
118
+ label: 'Referenece',
119
+ display: 'icon',
120
+ tooltip: 'Create reference',
121
+ // Ignored via go/ees005
122
+ // eslint-disable-next-line require-await
123
+ icon: async () => () => /*#__PURE__*/React.createElement(SmartLinkIcon, {
124
+ label: "Synced Block",
125
+ size: "medium"
126
+ }),
127
+ // Ignored via go/ees005
128
+ // eslint-disable-next-line require-await
129
+ action: async (contextNode, api) => {
130
+ var _contextNode$attrs, _contextNode$attrs2, _contextNode$attrs2$p, _contextNode$attrs3, _contextNode$attrs3$p;
131
+ api.doc.insertAfter((_contextNode$attrs = contextNode.attrs) === null || _contextNode$attrs === void 0 ? void 0 : _contextNode$attrs.localId, {
132
+ type: 'extension',
133
+ attrs: {
134
+ extensionType: SYNCED_BLOCK_EXTENSION_TYPE,
135
+ extensionKey: SYNCED_BLOCK_REFERENCE_KEY,
136
+ parameters: {
137
+ sourceDocumentAri: (_contextNode$attrs2 = contextNode.attrs) === null || _contextNode$attrs2 === void 0 ? void 0 : (_contextNode$attrs2$p = _contextNode$attrs2.parameters) === null || _contextNode$attrs2$p === void 0 ? void 0 : _contextNode$attrs2$p.sourceDocumentAri,
138
+ contentAri: (_contextNode$attrs3 = contextNode.attrs) === null || _contextNode$attrs3 === void 0 ? void 0 : (_contextNode$attrs3$p = _contextNode$attrs3.parameters) === null || _contextNode$attrs3$p === void 0 ? void 0 : _contextNode$attrs3$p.contentAri
139
+ },
140
+ localId: 'testId'
141
+ }
142
+ });
143
+ }
144
+ }]
145
+ }]
146
+ }
147
+ });
@@ -0,0 +1,19 @@
1
+ import { getCloudId } from '../constants';
2
+ export const getConfluencePageAri = pageId => `ari:cloud:confluence:${getCloudId()}:page/${pageId}`;
3
+ export const getPageIdFromAri = ari => {
4
+ // eslint-disable-next-line require-unicode-regexp
5
+ const match = ari.match(/ari:cloud:confluence:[^:]+:page\/(\d+)/);
6
+ if (match) {
7
+ return match[1];
8
+ }
9
+ throw new Error(`Invalid page ARI: ${ari}`);
10
+ };
11
+ export const getContentPropertyAri = contentPropertyId => `ari:cloud:confluence:${getCloudId()}:content/${contentPropertyId}`;
12
+ export const getContentPropertyIdFromAri = ari => {
13
+ // eslint-disable-next-line require-unicode-regexp
14
+ const match = ari.match(/ari:cloud:confluence:[^:]+:content\/([^/]+)/);
15
+ if (match) {
16
+ return match[1];
17
+ }
18
+ throw new Error(`Invalid content property ARI: ${ari}`);
19
+ };