Thread an optional collaborationProvider into ContentEditor

What does this MR do and why?

Adds an optional collaborationProvider prop that travels from MarkdownEditor through ContentEditor and into createContentEditor. It defaults to null. Pass nothing and everything behaves as it does today.

The provider is opaque to all the code in this MR. It is a duck-typed contract: the code reads .doc, .awareness, .whenSynced, .seed() and .identityFor(clientId). Nothing here constructs a provider. Nothing here opens a network connection. The concrete implementation, an ActionCable-backed Yjs provider, arrives in a later MR.

When a provider is passed:

  • createContentEditor swaps the tiptap History extension for Collaboration and CollaborationCursor. Yjs ships its own undo manager and tracks history per client, so the two must not both be loaded.
  • ContentEditor stops reacting to the markdown prop and hides the placeholder. The prop lags the CRDT document, so it is no longer a reliable source of truth.
  • setSerializedContent takes a collaborative path. It waits on provider.whenSynced, with a 10 second timeout so a session that never syncs rejects instead of hanging. Only the one client the server elected to seed writes the initial document. Every other client receives it through the CRDT.

A new createCollaborationExtensions configures the two tiptap extensions and supplies a custom cursor renderer. The MR also adds SCSS for the caret and its name label, the yjs, y-protocols, y-prosemirror, @tiptap/extension-collaboration and @tiptap/extension-collaboration-cursor dependencies, and a webpack babel-loader rule for the Yjs packages. Those packages ship untranspiled optional chaining and nullish coalescing, which the webpack 4 parser cannot read.

Nothing here is reachable by a user. There is no feature flag, no wiki wiring, no channel, and no way to obtain a provider. It is inert plumbing.

Part of a split

This is 1 of 6. The original MR, !250607 (closed), came in at around 2000 lines and was not feasible to review in one pass. This slice is the one @kivikakk asked for first: the provider as it travels through the components into ContentEditor, so the status quo can be verified on its own. It is around 430 lines and delivers that slice and nothing else.

The remaining slices:

  • 2: Gitlab::CollaborativeEditing::DocumentStore, with the Redis and rate-limiting review.
  • 3: CollaborativeEditing::BaseChannel and WikiPageChannel, where the AppSec review goes.
  • 4: the ActionCableProvider implementation.
  • 5: wiki wiring, CollaboratorsIndicator, and the feature flag that makes the feature reachable.

Verifying the status quo

With no provider passed, behaviour is unchanged. The History extension still loads, isCollaborative is false, and setSerializedContent takes its original path.

The existing suites pass untouched:

  • 1219 tests across spec/frontend/content_editor
  • 367 tests across spec/frontend/vue_shared/components/markdown
  • 27 tests across the three touched and added service specs

Security

The cursor renderer reads a collaborator's name and colour from provider.identityFor(clientId), never from the Yjs awareness state. The awareness payload is authored by the peer, so any name or avatar carried inside it would be whatever the sender chose. A user could impersonate a colleague. Reading identity from the server-stamped map closes that. A test asserts that a peer-authored name and colour are ignored.

The caret element carries aria-hidden="true" so screen readers do not read a collaborator's name as part of the document prose.

Cursor colour derives from the user id via assignUserColor, so a given person keeps the same colour across sessions and across clients. It falls back to a fixed blue when the server has stamped no identity yet.

References

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.


🤖 This content was generated by GitLab Duo.

Merge request reports

Loading
Loading