@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.
- package/CHANGELOG.md +12 -0
- package/LICENSE.md +11 -0
- package/README.md +1 -0
- package/SyncedBlock/package.json +15 -0
- package/afm-cc/tsconfig.json +44 -0
- package/build/tsconfig.json +22 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/pm-plugins/SyncClient.js +167 -0
- package/dist/cjs/pm-plugins/main.js +50 -0
- package/dist/cjs/pm-plugins/utils.js +19 -0
- package/dist/cjs/syncedBlockPlugin.js +24 -0
- package/dist/cjs/syncedBlockPluginType.js +5 -0
- package/dist/cjs/types/index.js +1 -0
- package/dist/cjs/ui/extensions/synced-block/components/GlobalStyles.js +25 -0
- package/dist/cjs/ui/extensions/synced-block/components/SyncedBlockLiveView.js +25 -0
- package/dist/cjs/ui/extensions/synced-block/components/SyncedBlockRenderer.js +25 -0
- package/dist/cjs/ui/extensions/synced-block/constants.js +32 -0
- package/dist/cjs/ui/extensions/synced-block/getSyncedBlockExtensionProvider.js +11 -0
- package/dist/cjs/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.js +29 -0
- package/dist/cjs/ui/extensions/synced-block/hooks/usePollContentProperty.js +121 -0
- package/dist/cjs/ui/extensions/synced-block/index.js +19 -0
- package/dist/cjs/ui/extensions/synced-block/manifest.js +254 -0
- package/dist/cjs/ui/extensions/synced-block/utils/ari.js +29 -0
- package/dist/cjs/ui/extensions/synced-block/utils/content-property.js +159 -0
- package/dist/cjs/ui/extensions/synced-block/utils/synced-block.js +65 -0
- package/dist/es2019/index.js +4 -0
- package/dist/es2019/pm-plugins/SyncClient.js +102 -0
- package/dist/es2019/pm-plugins/main.js +47 -0
- package/dist/es2019/pm-plugins/utils.js +13 -0
- package/dist/es2019/syncedBlockPlugin.js +17 -0
- package/dist/es2019/syncedBlockPluginType.js +1 -0
- package/dist/es2019/types/index.js +0 -0
- package/dist/es2019/ui/extensions/synced-block/components/GlobalStyles.js +18 -0
- package/dist/es2019/ui/extensions/synced-block/components/SyncedBlockLiveView.js +19 -0
- package/dist/es2019/ui/extensions/synced-block/components/SyncedBlockRenderer.js +19 -0
- package/dist/es2019/ui/extensions/synced-block/constants.js +26 -0
- package/dist/es2019/ui/extensions/synced-block/getSyncedBlockExtensionProvider.js +5 -0
- package/dist/es2019/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.js +24 -0
- package/dist/es2019/ui/extensions/synced-block/hooks/usePollContentProperty.js +107 -0
- package/dist/es2019/ui/extensions/synced-block/index.js +5 -0
- package/dist/es2019/ui/extensions/synced-block/manifest.js +147 -0
- package/dist/es2019/ui/extensions/synced-block/utils/ari.js +19 -0
- package/dist/es2019/ui/extensions/synced-block/utils/content-property.js +108 -0
- package/dist/es2019/ui/extensions/synced-block/utils/synced-block.js +57 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/pm-plugins/SyncClient.js +160 -0
- package/dist/esm/pm-plugins/main.js +44 -0
- package/dist/esm/pm-plugins/utils.js +13 -0
- package/dist/esm/syncedBlockPlugin.js +17 -0
- package/dist/esm/syncedBlockPluginType.js +1 -0
- package/dist/esm/types/index.js +0 -0
- package/dist/esm/ui/extensions/synced-block/components/GlobalStyles.js +18 -0
- package/dist/esm/ui/extensions/synced-block/components/SyncedBlockLiveView.js +18 -0
- package/dist/esm/ui/extensions/synced-block/components/SyncedBlockRenderer.js +18 -0
- package/dist/esm/ui/extensions/synced-block/constants.js +26 -0
- package/dist/esm/ui/extensions/synced-block/getSyncedBlockExtensionProvider.js +5 -0
- package/dist/esm/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.js +23 -0
- package/dist/esm/ui/extensions/synced-block/hooks/usePollContentProperty.js +114 -0
- package/dist/esm/ui/extensions/synced-block/index.js +5 -0
- package/dist/esm/ui/extensions/synced-block/manifest.js +247 -0
- package/dist/esm/ui/extensions/synced-block/utils/ari.js +23 -0
- package/dist/esm/ui/extensions/synced-block/utils/content-property.js +153 -0
- package/dist/esm/ui/extensions/synced-block/utils/synced-block.js +58 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/pm-plugins/SyncClient.d.ts +14 -0
- package/dist/types/pm-plugins/main.d.ts +6 -0
- package/dist/types/pm-plugins/utils.d.ts +5 -0
- package/dist/types/syncedBlockPlugin.d.ts +2 -0
- package/dist/types/syncedBlockPluginType.d.ts +2 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/ui/extensions/synced-block/components/GlobalStyles.d.ts +6 -0
- package/dist/types/ui/extensions/synced-block/components/SyncedBlockLiveView.d.ts +7 -0
- package/dist/types/ui/extensions/synced-block/components/SyncedBlockRenderer.d.ts +7 -0
- package/dist/types/ui/extensions/synced-block/constants.d.ts +8 -0
- package/dist/types/ui/extensions/synced-block/getSyncedBlockExtensionProvider.d.ts +2 -0
- package/dist/types/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.d.ts +6 -0
- package/dist/types/ui/extensions/synced-block/hooks/usePollContentProperty.d.ts +7 -0
- package/dist/types/ui/extensions/synced-block/index.d.ts +2 -0
- package/dist/types/ui/extensions/synced-block/manifest.d.ts +2 -0
- package/dist/types/ui/extensions/synced-block/utils/ari.d.ts +4 -0
- package/dist/types/ui/extensions/synced-block/utils/content-property.d.ts +33 -0
- package/dist/types/ui/extensions/synced-block/utils/synced-block.d.ts +24 -0
- package/dist/types-ts4.5/index.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/SyncClient.d.ts +14 -0
- package/dist/types-ts4.5/pm-plugins/main.d.ts +6 -0
- package/dist/types-ts4.5/pm-plugins/utils.d.ts +5 -0
- package/dist/types-ts4.5/syncedBlockPlugin.d.ts +2 -0
- package/dist/types-ts4.5/syncedBlockPluginType.d.ts +2 -0
- package/dist/types-ts4.5/types/index.d.ts +3 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/components/GlobalStyles.d.ts +6 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/components/SyncedBlockLiveView.d.ts +7 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/components/SyncedBlockRenderer.d.ts +7 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/constants.d.ts +8 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/getSyncedBlockExtensionProvider.d.ts +2 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/hooks/useLiveSyncedBlockContent.d.ts +6 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/hooks/usePollContentProperty.d.ts +7 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/index.d.ts +2 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/manifest.d.ts +2 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/utils/ari.d.ts +4 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/utils/content-property.d.ts +33 -0
- package/dist/types-ts4.5/ui/extensions/synced-block/utils/synced-block.d.ts +24 -0
- package/docs/0-intro.tsx +43 -0
- package/package.json +88 -0
- 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,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,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
|
+
};
|