@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
517 lines (516 loc) • 18.8 kB
TypeScript
import type { ReactElement } from 'react';
import type { SerializedStyles } from '@emotion/react';
import type { BatchAttrsStep, OverrideDocumentStepJSON as OverrideDocumentStep } from '@atlaskit/adf-schema/steps';
import type { JSONDocNode } from '@atlaskit/editor-json-transformer';
import type { Node as PMNode, Slice } from '@atlaskit/editor-prosemirror/model';
import type { EditorState, ReadonlyTransaction, Transaction } from '@atlaskit/editor-prosemirror/state';
import type { Step } from '@atlaskit/editor-prosemirror/transform';
import { token } from '@atlaskit/tokens';
import type { Providers } from '../provider-factory';
import type { GetResolvedEditorStateReason } from '../types';
export type NewCollabSyncUpErrorAttributes = {
clientId?: number | string;
lengthOfUnconfirmedSteps?: number;
maxRetries: number;
tries: number;
version: number;
};
export type ResolvedEditorState<T = any> = {
content: JSONDocNode | T;
stepVersion: number;
title: string | null;
};
export declare enum PROVIDER_ERROR_CODE {
NO_PERMISSION_ERROR = "NO_PERMISSION_ERROR",
INVALID_USER_TOKEN = "INVALID_USER_TOKEN",
DOCUMENT_NOT_FOUND = "DOCUMENT_NOT_FOUND",
LOCKED = "LOCKED",
FAIL_TO_SAVE = "FAIL_TO_SAVE",
DOCUMENT_RESTORE_ERROR = "DOCUMENT_RESTORE_ERROR",
INITIALISATION_ERROR = "INITIALISATION_ERROR",
NETWORK_ISSUE = "NETWORK_ISSUE",
INVALID_PROVIDER_CONFIGURATION = "INVALID_PROVIDER_CONFIGURATION",
INTERNAL_SERVICE_ERROR = "INTERNAL_SERVICE_ERROR",
DOCUMENT_UPDATE_ERROR = "DOCUMENT_UPDATE_ERROR"
}
/**
* This occurs when the provided user token is considered invalid for the given document ARI.
* It happens during initialisation of the provider.
* It could mean the document has been deleted (hence not found).
* @message Message returned to editor, i.e User does not have permissions to access this document or document is not found
* @recoverable It is recoverable, as we will try to refresh the token.
*/
type InsufficientEditingPermission = {
code: PROVIDER_ERROR_CODE.NO_PERMISSION_ERROR;
message: string;
reason?: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* Similar to InsufficientEditingPermission, but the user token is invalid because it has expired or been revoked.
* It may also be an invalid token format.
* This error is given to the provider by NCS.
* @message Message returned to editor, i.e. The user token was invalid
* @recoverable It is recoverable, as we will try to refresh the token.
*/
type InvalidUserToken = {
code: PROVIDER_ERROR_CODE.INVALID_USER_TOKEN;
message: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* Document not found error, thrown when the provider is unable to find a document with the given ARI and user token.
* It occurs during fetchCatchup, a function that fetches the latest document state during catchup.
* We need to recieve a 404 from the document service to throw this error.
* @message Message returned to editor, i.e. The requested document is not found
* @recoverable It is recoverable, as the provider can try again later.
*/
type DocumentNotFound = {
code: PROVIDER_ERROR_CODE.DOCUMENT_NOT_FOUND;
message: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* This error is thrown when the document is locked by another user.
* The error is passed to us by NCS.
* @message Message returned to editor, i.e. The document is currently not available, please try again later
* @recoverable It is recoverable, as the provider can try again later.
*/
type Locked = {
code: PROVIDER_ERROR_CODE.LOCKED;
message: string;
recoverable: boolean;
status?: number;
};
/**
* This error is thrown when the provider is unable to save the document.
* This can happen when the connection to dynamoDB is lost, or when we do not have sufficient permissions (DYNAMO ERROR).
* This error is given to us by NCS.
* @message Message returned to editor, i.e. Collab service is not able to save changes
* @recoverable It is not recoverable, as we don't want the user to continue editing a document that is not being saved.
*/
type FailToSave = {
code: PROVIDER_ERROR_CODE.FAIL_TO_SAVE;
message: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* This error is thrown when the provider is unable to restore the document.
* It occurs during onRestore, a function that restores the document to a previous version and reapplies unconfirmed steps.
* onRestore is called when page recovery has emitted an 'init' event on a page client is currently connected to.
* It could mean we failed to update the page metadata, or we failed to reapply unconfirmed steps.
* @message Message returned to editor, i.e. Collab service unable to restore document
* @recoverable It is not recoverable, as the provider has no further options after this.
* The user will need to refresh the page to try again.
*/
type DocumentNotRestore = {
code: PROVIDER_ERROR_CODE.DOCUMENT_RESTORE_ERROR;
message: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* The initial document couldn't be loaded from the collab service.
* This error is given to us by NCS.
* It could indicate either a network issue, or an internal service error in NCS.
* @message Message returned to editor, i.e. The initial document couldn't be loaded from the collab service
* @recoverable It is not recoverable, as the provider cannot do anything to fix it.
* The user will need to refresh the page to try again.
*/
type InitialisationError = {
code: PROVIDER_ERROR_CODE.INITIALISATION_ERROR;
message: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* Couldn't reconnect to the collab service (NCS) due to network issues.
* NCS could be down, or the user could be offline. It's also possible the url is incorrect, or the user is behind a proxy blocking the connection.
* Fired upon a reconnection attempt error (from Socket.IO Manager)
* @message Message returned to editor, i.e. Couldn't reconnect to the collab service due to network issues
* @recoverable It is recoverable, as the provider will try to reconnect.
*/
type NetworkIssue = {
code: PROVIDER_ERROR_CODE.NETWORK_ISSUE;
message: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* This error is thrown when the provider has an invalid configuration.
* It could happen due to these errors from NCS:
* NAMESPACE_INVALID
INVALID_ACTIVATION_ID
INVALID_DOCUMENT_ARI
INVALID_CLOUD_ID
* @message Message returned to editor, i.e. Invalid provider configuration
* @recoverable It is not recoverable, as the provider cannot do anything to fix it.
* The service using the provider will need to fix the configuration.
*/
type InvalidProviderConfiguration = {
code: PROVIDER_ERROR_CODE.INVALID_PROVIDER_CONFIGURATION;
message: string;
reason: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* This error is thrown when the provider encounters an internal service error, not otherwise accounted for.
* @message Message returned to editor, i.e. Collab Provider experienced an unrecoverable error
* @recoverable It is not recoverable, as the provider cannot do anything to fix it.
*/
type InternalServiceError = {
code: PROVIDER_ERROR_CODE.INTERNAL_SERVICE_ERROR;
message: string;
reason: string;
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
type ProviderDocumentUpdateError = {
code: PROVIDER_ERROR_CODE.DOCUMENT_UPDATE_ERROR;
message: 'The provider failed to apply changes to the editor';
recoverable: boolean;
/**
* @deprecated switch to using either the error code or the recoverable flag
*/
status?: number;
};
/**
* A union of all possible provider errors that can be emitted back to the editor.
*/
export type ProviderError = InsufficientEditingPermission | InvalidUserToken | DocumentNotFound | Locked | FailToSave | DocumentNotRestore | InitialisationError | NetworkIssue | InvalidProviderConfiguration | InternalServiceError | ProviderDocumentUpdateError;
export interface Metadata {
[key: string]: string | number | boolean;
}
export type CollabMetadataPayload = Metadata;
export interface CollabEventInitData {
doc?: any;
json?: any;
reserveCursor?: boolean;
sid?: string;
version?: number;
}
export interface CollabInitPayload extends CollabEventInitData {
caller?: string;
doc: any;
metadata?: Metadata;
reserveCursor?: boolean;
targetClientId?: string;
version: number;
}
export interface CollabEventConnectionData {
initial: boolean;
sid: string;
}
export type CollabConnectedPayload = CollabEventConnectionData;
export declare enum DisconnectReason {
CLIENT_DISCONNECT = "CLIENT_DISCONNECT",
SERVER_DISCONNECT = "SERVER_DISCONNECT",
SOCKET_CLOSED = "SOCKET_CLOSED",
SOCKET_ERROR = "SOCKET_ERROR",
SOCKET_TIMEOUT = "SOCKET_TIMEOUT",
UNKNOWN_DISCONNECT = "UNKNOWN_DISCONNECT"
}
export interface CollabDisconnectedPayload {
reason: DisconnectReason;
sid: string;
}
export interface CollabNamespaceLockCheckPayload {
isLocked: boolean;
}
export interface CollabEventRemoteData {
json?: any;
newState?: EditorState;
userIds?: (number | string)[];
}
type MarkJson = {
attrs: {
[key: string]: any;
};
type: string;
};
export type NodeJson = {
attrs: {
[key: string]: any;
};
content: NodeJson[];
marks: MarkJson[];
text?: string;
type: string;
};
type SliceJson = {
content: NodeJson[];
openEnd: number;
openStart: number;
};
export interface StepMetadata {
metadata?: {
createdOffline?: boolean;
prevStepId?: string;
rebased?: boolean;
reqId?: string;
schemaVersion?: string;
source?: string;
stepId?: string;
traceId?: string;
unconfirmedStepAfterRecovery?: boolean;
};
}
export interface BaseStepPM extends StepMetadata {
clientId: number | string;
from?: number;
slice?: SliceJson;
stepType: string;
to?: number;
userId: string;
}
export interface ReplaceStepPM extends BaseStepPM {
from: number;
slice: SliceJson;
structure?: boolean;
to: number;
}
export interface ReplaceAroundStepPM extends BaseStepPM {
from: number;
gapFrom: number;
gapTo: number;
insert: number;
slice: SliceJson;
structure?: boolean;
to: number;
}
export type InlineCommentStepPM = InlineCommentAddMarkStepPM | InlineCommentAddNodeMarkStepPM;
interface InlineCommentAddMarkStepPM extends BaseStepPM {
from: number;
mark?: {
attrs?: {
annotationType?: 'inlineComment';
id?: string;
};
type?: 'annotation';
};
stepType: 'addMark';
to: number;
}
export interface InlineCommentAddNodeMarkStepPM extends BaseStepPM {
mark?: {
attrs?: {
annotationType?: 'inlineComment';
id?: string;
};
type?: 'annotation';
};
pos: number;
stepType: 'addNodeMark';
}
export interface SetAttrsStepPM extends BaseStepPM {
attrs: Record<string, unknown>;
pos: number;
stepType: 'setAttrs';
}
export type BatchAttrsStepPM = BaseStepPM & BatchAttrsStep;
export type OverrideDocumentStepPM = BaseStepPM & OverrideDocumentStep;
export type StepJson = OverrideDocumentStepPM | ReplaceAroundStepPM | ReplaceStepPM | InlineCommentStepPM | SetAttrsStepPM;
export interface CollabDataPayload extends CollabEventRemoteData {
json: StepJson[];
userIds: (number | string)[];
version: number;
}
export interface CollabSendableSelection {
anchor?: number | string;
head?: number | string;
type: 'textSelection' | 'nodeSelection';
}
export type PresenceActivity = 'viewer' | 'editor';
export type CollabActivityAIProviderChangedPayload = {
action: 'add' | 'remove';
providerId?: string;
type: 'ai-provider:change';
};
export type CollabPresenceActivityChangePayload = {
activity?: PresenceActivity;
type: 'participant:activity';
};
export interface CollabEventTelepointerData {
selection: CollabSendableSelection;
sessionId: string;
type: 'telepointer';
}
export type CollabTelepointerPayload = CollabEventTelepointerData;
type ProviderParticipantPermitLevel = {
isPermittedToComment?: boolean;
isPermittedToEdit?: boolean;
isPermittedToView?: boolean;
};
export interface CollabParticipant {
avatar: string;
cursorPos?: number;
isGuest?: boolean;
isHydrated?: boolean;
lastActive: number;
name: string;
permit?: ProviderParticipantPermitLevel;
presenceActivity?: PresenceActivity;
presenceId?: string;
sessionId: string;
}
export type ProviderParticipant = CollabParticipant & {
clientId: number | string;
email: string;
userId: string;
};
export interface CollabEventPresenceData {
joined?: ProviderParticipant[];
left?: {
sessionId: string;
}[];
}
export type CollabPresencePayload = CollabEventPresenceData;
export type CollabLocalStepsPayload = {
steps: readonly Step[];
};
export interface CollabEventConnectingData {
initial: boolean;
}
export type CollabConnectingPayload = CollabEventConnectingData;
export type CollabCommitStatusEventPayload = {
status: 'attempt' | 'success' | 'failure';
version: number;
};
export type UserPermitType = {
isPermittedToComment: boolean;
isPermittedToEdit: boolean;
isPermittedToView: boolean;
};
export type CollabPermissionEventPayload = UserPermitType;
export type ConflictChange = {
from: number;
local: Slice;
remote: Slice;
to: number;
};
export type ConflictChanges = {
deleted: ConflictChange[];
inserted: ConflictChange[];
};
export interface CollabEventConflictPayload extends ConflictChanges {
offlineDoc: PMNode;
}
export interface CollabEvents {
'commit-status': CollabCommitStatusEventPayload;
connected: CollabConnectedPayload;
connecting: CollabConnectingPayload;
data: CollabDataPayload;
'data:conflict': CollabEventConflictPayload;
disconnected: CollabDisconnectedPayload;
entity: any;
error: ProviderError;
init: CollabInitPayload;
'local-steps': CollabLocalStepsPayload;
'metadata:changed': Metadata;
'namespace-lock:check': CollabNamespaceLockCheckPayload;
permission: CollabPermissionEventPayload;
presence: CollabPresencePayload;
'presence:changed': CollabPresenceActivityChangePayload;
telepointer: CollabTelepointerPayload;
}
export type SyncUpErrorFunction = (attributes: NewCollabSyncUpErrorAttributes) => void;
export interface CollabEditProvider<Events extends CollabEvents = CollabEvents> {
getFinalAcknowledgedState: (reason: GetResolvedEditorStateReason) => Promise<ResolvedEditorState>;
/**
* Returns the cached `init` payload if the provider has already initialised the
* document with NCS, otherwise `undefined`.
*
* Used by the collab plugin to seed a freshly-attached plugin view (e.g. after
* an editor preset reconfigure or a full EditorView recreation) with the same
* `init` data the original subscribers received. Without this, late subscribers
* never receive `init` (it is fired once at session start) and the editor
* gets stuck in the `!isReady` state, silently dropping doc-changing
* transactions via `filterTransaction`.
*
* Optional for backwards compatibility with custom provider implementations
* (e.g. test mocks). When undefined, the rebind path is skipped.
*/
getInitPayload?: () => CollabEventInitData | undefined;
getIsNamespaceLocked: () => boolean;
initialize: (getState: () => any, createStep: (json: object) => Step) => this;
off: (evt: keyof Events, handler: (...args: any) => void) => this;
on: (evt: keyof Events, handler: (...args: any) => void) => this;
send: (tr: Transaction, oldState: EditorState, newState: EditorState) => void;
sendMessage: <K extends keyof Events>(data: {
type: K;
} & Events[K]) => void;
setup: (props: {
editorApi?: any;
getState?: () => EditorState;
onSyncUpError?: SyncUpErrorFunction;
}) => this;
unsubscribeAll: (evt: keyof Events) => this;
}
export type CollabEditOptions = {
provider?: Providers['collabEditProvider'];
useNativePlugin?: boolean;
userId?: string;
} & CollabInviteToEditProps & CollabAnalyticsProps;
export type InviteToEditButtonProps = {
onClick: (event: React.MouseEvent<HTMLElement>) => void;
selected: boolean;
};
export type InviteToEditComponentProps = {
children: ReactElement<InviteToEditButtonProps>;
};
export interface CollabInviteToEditProps {
inviteToEditComponent?: React.ComponentType<React.PropsWithChildren<InviteToEditComponentProps>>;
inviteToEditHandler?: (event: React.MouseEvent<HTMLElement>) => void;
isInviteToEditButtonSelected?: boolean;
}
export interface CollabAnalyticsProps {
/**
* @description Control whether Synchrony entity error events are tracked
*/
EXPERIMENTAL_allowInternalErrorAnalytics?: boolean;
}
export interface CollabEventLocalStepData {
steps: Array<Step>;
}
export type Color = ReturnType<typeof token>;
export declare const TELEPOINTER_DIM_CLASS = "telepointer-dim";
export declare const TELEPOINTER_PULSE_CLASS = "telepointer-pulse-animate";
export declare const TELEPOINTER_PULSE_DURING_TR_CLASS = "telepointer-pulse-during-tr";
export declare const TELEPOINTER_PULSE_DURING_TR_DURATION_MS = 7500;
export declare const TELEPOINTER_DATA_SESSION_ID_ATTR = "data-telepointer-sessionid";
export declare const telepointerStyle: SerializedStyles;
export declare const isDirtyTransaction: (tr: Transaction | ReadonlyTransaction) => boolean;
export declare const tintDirtyTransaction: (tr: Transaction) => void;
export {};