Availability | Confluence 6.0 or later |
Collaborative editing allows multiple people to concurrently edit a single Confluence page or blog post (we'll just call them pages from here on). When using collaborative editing, a page editor will see the avatar(s) of others editing the page, and the changes they make will appear in the editor in real time.
This page provides an overview of the various components that make up collaborative editing in Confluence Data Center. We'll go through how each component works, and how you can build plugins to integrate with them.
If you're developing for Confluence Cloud head to Collaborative editing for Confluence Cloud.
We're changing the way drafts behave, and moving to a model that's more consistent with how content should be stored for collaborative editing. We'll call the outgoing model, used prior to collaborative editing, 'personal drafts', and the new model 'shared drafts'. The notable differences are:
Draft
class, which extends ContentEntityObject
and contains a draftType
field indicating whether the draft was a "page"
or "blogpost"
. We're deprecating the Draft
class in favour of using the existing Page
and Blogpost
classes, where a contentStatus
field of "draft"
will indicate the object is a draft.Comparing the personal draft and shared draft models:
Personal drafts (before collaborative editing) |
---|
Shared drafts (collaborative editing) |
A draft page object in its different representations:
Personal draft |
---|
Class: ContentStatus: N/A DraftType: " |
Shared draft |
Class: ContentStatus: " DraftType: N/A |
We provide a REST API that allows external interaction with shared drafts. The currently-supported operations are:
The Drafts API extends Confluence's existing REST API. In order to reference the draft, you'll need to add the following query parameter to REST requests:
1 2?status=draft
For example, if the request to fetch an existing page is:
1 2GET /rest/api/content/{contentId}
The request to fetch the corresponding draft will be:
1 2GET /rest/api/content/{contentId}?status=draft
Similarly, publishing a draft will be:
1 2PUT /rest/api/content/{contentId}?status=draft
where the request payload will contain:
1 2{ ... "status": "current" ... }
indicating that the status should be updated from "draft
" to "current
".
When publishing drafts, it's also possible to specify a "conflictPolicy
" query parameter. Currently only the "abort
" policy is supported. This means that the publish operation will be aborted (and an error code returned) if a conflict is encountered during the publishing of a draft.
You can find more detailed documentation for the Confluence Data Center REST API at https://docs.atlassian.com/atlassian-confluence/REST/latest-server/
Draft operations are also exposed as a Java interface via the ContentService
and ContentDraftService
. These services are made available to plugins as Spring Beans with apiContentService
and apiContentDraftService
IDs.
You can create a shared draft by calling:
1 2contentService .create(Content .builder() .status(ContentStatus.DRAFT) ... .build());
Note that the API currently only supports creating new shared drafts - it doesn't support creating a shared draft for an existing page.
To fetch a shared draft:
1 2contentService .find() .withStatus(ContentStatus.DRAFT) ... .fetchOne();
Finally, to publish a draft:
1 2contentDraftService .publishEditDraft(Content .builder() .status(ContentStatus.CURRENT) ... .build(), ConflictPolicy.ABORT);
Event | Purpose |
---|---|
SharedDraftCreatedEvent | Triggered when a new shared draft gets created. Type: Java. Example: Event Listener Module. |
SharedDraftUpdatedEvent | Triggered when a shared draft is updated. This normally happens at regular intervals in an editing session when a draft is auto-saved. Type: Java. Example: Event Listener Module. |
rte-draft-saved | Triggered when a draft is saved. This is similar to Type: Javascript. Example: AJS.bind('rte-draft-saved', <handler function>);. |
Shared draft support is provided to actions that extend the AbstractCreateAndEditPageAction
.
Prior to shared drafts, actions could access drafts by calling getDraft()
. This method - to be deprecated - only returns the old Draft
class, which is incompatible with shared drafts. In order to support shared drafts you need to replace calls to getDraft
with getDraftAsCEO
, which will return a shared draft on instances that support them or a personal draft on other instances. This allows actions to maintain compatibility with instances that haven't been upgraded to collaborative editing.
You should ensure that future calls are made to getContentDraft
, which will only return shared drafts and doesn't provide support for personal drafts.
To find out if shared drafts are supported using client-side Javascript:
1 2AJS.Meta.getBoolean('shared-drafts')
Personal drafts will still be accessible from users' drafts list. They won't, however, be able to resume editing the draft in the editor. Opening a personal draft will display it in a read-only dialog, from which the user can copy the content into a new page.
A user's drafts list will be populated with the following drafts:
Drafts that have a corresponding published page won't appear in the drafts list - users can access those drafts by editing the corresponding page.
When a page is reverted, the corresponding shared draft is also updated to contain the new page content. This will also trigger an 'external changes' request (see our explanation of external changes).
Shared drafts will now implement the hierarchical interface (inherited from the Page
and Blogpost
classes), which means it's now possible to persist location onto drafts.
Drafts created using the AbstractCreateAndEditPageAction
or DraftsTransitionHelper
will:
We've also updated the move page dialog so that it persists the draft's location by calling MovePageAction
rather than temporarily storing the location on the client. This means the draft location will persist between different editing sessions.
There are two scenarios for draft metadata:
Scenario | Metadata persistence |
---|---|
A shared draft has never been published |
|
A shared draft has been published and as such links with a corresponding page |
|
To collaborate on a shared draft that's linked to a published page, users only need to edit the published page.
To access a shared draft that's never been published, you need two pieces of information:
contentId
(this will also be the draftId
for unpublished drafts)draftShareId
of the draftAt the time a draft is created, a draftShareId
is generated and stored against the draft's content properties. This content and draftShareId
are displayed in the URL while the draft is being edited. It'll look like this:
1 2<your_confluence_URL>/pages/resumedraft.action?draftId=1234&draftShareId=e4d05ef0-3cef-3c63-9f44-15e899469ad2
Sharing this link with other users will allow them to edit the same draft, assuming the view and edit restrictions on the draft allow it.
Once a user accesses a draft with the correct draftShareId
they'll be granted a ContentPermission#SHARED_PERMISSION
, which allows them to make future edits to the draft without providing the draftShareId
. This shared permission won't be visible in the UI, and it'll be removed once the draft is published.
Stricter rules now apply to draft versioning. Whereas it was possible to save drafts as new versions prior to collaborative editing, this action is no longer permitted. Drafts must have a version of 1. If a version bump of a draft is attempted, Confluence will throw an exception.
When saving a draft, don't call PageManager#saveNewVerion
Shared drafts, like personal drafts, aren't indexed in Confluence. Since they're not added to the content index, it means drafts won't appear in features that access the context index, including:
Page updates triggered by draft publishes will appear in the content index as usual.
In Confluence Data Center we provide administrators with a choice of three collaborative editing modes:
Changing the mode is always initiated by the administrator. We do not automatically fall back to a particular mode at any time. You may want to test your add-on in all three modes.
Make sure:
Synchrony is a service that allows the synchronisation of arbitrary data models in real time. It supports special synchronisation for HTML WYSIWYG editors, including telepointers (remote selections).
appId
and appSecret
contentId
of the page being editedThis means that content data will be stored on the Synchrony service and the service will act as the source of truth for page content. While this is the case, Confluence will continue storing a snapshot of the most recent state of the document being edited at regular intervals. This content is stored as the body content in the shared draft but shouldn't be referenced as the most up-to-date content, as changes stored in Synchrony may not have been saved to the draft yet.
Synchrony is executed as a separate process by Confluence Data Center. This process is managed by Confluence automatically, and through out normal operations, is not intended to be managed manually by an administrator. Administrators can choose to change the editing mode (on, limited, off). The affect of this change is site-wide. Collaborative editing cannot be turned on or off for specific spaces or pages.
The Synchrony Service executable is extracted from Confluence into the home directory. It will be named 'synchrony-standalone.jar
', and a second java process will be spawned with the above mentioned jar
as the entry point. Confluence passes all required parameters for Synchrony via environmental variables - these are not considered api, and may be subject to change.
Synchrony requires a JDBC connection to the database that Confluence Data Center runs under. The three database tables used by Synchrony are 'Events', 'Secrets' and 'Snapshots'. Modifying the contents of these tables is not recommended, and they are subject to change without notice.
Synchrony doesn't currently provide an API for third party plugins. A client-side implementation is already provided by the Confluence Collaborative Editor Plugin, which will:
When a session is opened to Synchrony, a whitelist of styles, elements and attributes is provided. The whitelist prevents malicious content from being synchronised from one client to another. Any items not in the whitelist will be ignored and won't be synchronised to other editing sessions.
1 2'styles': $.extend({}, Synchrony.whitelists.tinymce.styles, { // Syncing the padding-top on the body element will // cause an infinite sync loop (WD-170). Other // padding-* styles are blacklisted for // completeness. 'padding-top': false, 'padding-right': false, 'padding-bottom': false, 'padding-left': false, 'padding': false, 'display': cssQuiteSafeValueRx, 'list-style-type': cssQuiteSafeValueRx, 'background-image': true }), 'attributes': $.extend({}, Synchrony.whitelists.tinymce.attributes, { // ** date lozenge support // TODO this is only a workaround, the // contenteditable atribute should not be synced 'contenteditable': true, // -- end date lozenge support // ** inline comments support 'data-ref': true, // -- end inline comments support //Extra HTML attributes CONF uses 'accesskey' : true, 'datetime': true, 'data-highlight-class':true, 'data-highlight-colour':true, 'data-space-key':true, 'data-username': true, // ** space-list macro 'data-entity-id': true, 'data-entity-type': true, 'data-favourites-bound': true, // -- end space-list macro // ** macro placeholder support 'data-macro-id': true, 'data-macro-name': true, 'data-macro-schema-version': true, 'data-macro-body-type': true, 'data-macro-parameters': true, 'data-macro-default-parameter': true, // -- end macro placeholder support // ** page layout support 'data-atlassian-layout': true, 'data-placeholder-type': true, 'data-layout': true, 'data-title': true, 'data-type': true, // -- end page layout support // ** inline task support 'data-inline-task-id': true, // -- end inline task support // ** WD-323 'data-base-url': true, 'data-linked-resource-id': true, 'data-linked-resource-type': true, 'data-linked-resource-version': true, 'data-linked-resource-default-alias': true, 'data-linked-resource-container-version': true, // -- end WD-323 'data-unresolved-comment-count': true, 'data-location': true, 'data-image-height': true, 'data-image-width': true, 'data-attachment-copy': true // -- end WD-323 }), 'elements': $.extend({}, Synchrony.whitelists.tinymce.elements, { // ** date lozenge support 'time': true, // -- end date lozenge support 'label': true, 'form': true })
Depending on how Synchrony is disabled:
As Confluence opens a WebSocket connection to Synchrony, all Synchrony traffic can be monitored in the WebSocket request via the developer tools' Network tab:
External changes refer to updates made to page content outside of the Confluence editor, like inline comments and tasks. When external changes are made, the changes need to be propagated to Synchrony to ensure that Confluence and Synchrony are in sync.
External changes can be triggered in a number of ways. A plugin can:
ContentService
PageManager
It's possible to externally modify page content using other means, but it's important to stick with the methods listed above and only update published pages to ensure the changes are correctly propagated to Synchrony. Things to be aware of:
ContentEntityManager
to update page content - this'll bypass the external changes call to Synchrony.Synchrony provides an API that allows external applications to inform Synchrony of external changes. Confluence automatically makes a call to the Synchrony API whenever page content is updated, so plugins should avoid calling the Synchrony API directly.
If a plugin needs to modify page content, it just needs to make the appropriate updates in Confluence and the updates are pushed to Synchrony.
The external changes call to the Synchrony API will be made from PageManager#saveContentEntity
.
If page updates are made which bypass the PageManager
, then updates won't be synchronised to Synchrony.
Changes to the following will be propagated to Synchrony:
Event | Purpose |
---|---|
editor.external.change | Triggered after an external change has occurred and the changes have been synchronised to the editor Also triggered once after the content in the editor is initialised by Synchrony. Type: JavaScript. Example: AJS.bind('editor.external.change', <handler function>);. |
Don't:
ContentEntityManager
to directly modify page content - the external changes call will be bypassed. Always use the PageManager
.Rate this page: