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. We'll go through how each component works, and how you can build apps to integrate with them.
Developing for Confluence Server? Head to Collaborative editing for Confluence Server.
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 favor 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 | Shared draft |
---|---|
Class: ContentStatus: N/A DraftType: | 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.
Draft operations are also exposed as a Java interface via the ContentService
and ContentDraftService
. These services are made available to apps 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 | Type | Purpose | Example |
---|---|---|---|
SharedDraftCreatedEvent | Java | Triggered when a new shared draft gets created. | Event Listener Module |
SharedDraftUpdatedEvent | Java | Triggered when a shared draft is updated. This normally happens at regular intervals in an editing session when a draft is auto-saved. | Event Listener Module |
rte-draft-saved | Javascript | Triggered when a draft is saved. This is similar to SharedDraftUpdatedEvent , except that it's fired from the client side and is triggered for both personal and shared drafts. | 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')
Once shared drafts have been rolled out to instances, they won't be disabled.
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.
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.
Make sure:
Synchrony is a micro service that allows the synchronization of arbitrary data models in real time. It supports special synchronization 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.
The connection to Synchrony is configured via the 'Confluence Collaborative Editor Plugin'. There are four editable parameters on the app configuration screen:
The first three parameters must be correct in order to make a successful connection to Synchrony. The toggle allows Confluence admins to manually enable or disable the connection to the Synchrony service. Parameters can either be modified using the admin screen or a request to the SynchronyConfigurationAction
with the following form data:
appId
appSecret
serviceUrl
synchronyEnabled
The Confluence Collaborative Editor Plugin can also be configured via system properties:
Property | Description | Example |
---|---|---|
| Where the app expects the Synchrony service to be. Trailing forward slash is optional. Default: http://localhost:10123/ | https://synchrony.atlassian.io/ |
| Whether a debug Default: false | true |
| A list of instance hosts that should ignore the debug flag above. Useful when multiple instances are configured by the same system properties and you need to make exceptions. Yay snowflakes! Default: none | https://confluence.atlassian.com |
Synchrony doesn't currently provide an API for third-party apps. 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 synchronized from one client to another. Any items not in the whitelist will be ignored and won't be synchronized 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 web socket connection to Synchrony, all Synchrony traffic can be monitored in the web socket 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. An app can:
ContentService
.PageManager
.Don't use ContentEntityManager
to update page content --- this'll bypass the external changes call to Synchrony.
Don't make external changes to the draft object, as these changes will be lost when the published page is synchronized with 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 apps should avoid calling the Synchrony API directly.
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 synchronized to Synchrony.
Changes to the following will be propagated to Synchrony:
Event | Type | Purpose | Example |
---|---|---|---|
editor.external.change | Javascript | Triggered after an external change has occurred and the changes have been synchronized to the editor Also triggered once after the content in the editor is initialized by Synchrony. | 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
.Test apps with two open tabs, as this forces the synchronized changes to be filtered by the whitelist.
Test that external changes are correctly propagated by triggering another publish from the shared draft after the external change is made. This is to check that the change is still there after the publish.
Rate this page: