[WEB-1435] dev: conflict free issue descriptions (#5912)
* chore: new description binary endpoints * chore: conflict free issue description * chore: fix submitting status * chore: update yjs utils * chore: handle component re-mounting * chore: update buffer response type * chore: add try catch for issue description update * chore: update buffer response type * chore: description binary in retrieve * chore: update issue description hook * chore: decode description binary * chore: migrations fixes and cleanup * chore: migration fixes * fix: inbox issue description * chore: move update operations to the issue store * fix: merge conflicts * chore: reverted the commit * chore: removed the unwanted imports * chore: remove unnecessary props * chore: remove unused services * chore: update live server error handling --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
parent
229610513a
commit
e9680cab74
65 changed files with 1466 additions and 358 deletions
|
|
@ -8,7 +8,7 @@ import { IssueWidget } from "@/extensions";
|
|||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
|
||||
import { useCollaborativeDocumentEditor } from "@/hooks/use-collaborative-document-editor";
|
||||
// types
|
||||
import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types";
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
|||
}
|
||||
|
||||
// use document editor
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeDocumentEditor({
|
||||
onTransaction,
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { IssueWidget } from "@/extensions";
|
|||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useReadOnlyCollaborativeEditor } from "@/hooks/use-read-only-collaborative-editor";
|
||||
import { useCollaborativeDocumentReadOnlyEditor } from "@/hooks/use-collaborative-document-read-only-editor";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/types";
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn
|
|||
);
|
||||
}
|
||||
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeDocumentReadOnlyEditor({
|
||||
editorClassName,
|
||||
extensions,
|
||||
fileHandler,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Editor, Extension } from "@tiptap/core";
|
||||
import { AnyExtension, Editor } from "@tiptap/core";
|
||||
// components
|
||||
import { EditorContainer } from "@/components/editors";
|
||||
// constants
|
||||
|
|
@ -12,7 +12,7 @@ import { EditorContentWrapper } from "./editor-content";
|
|||
|
||||
type Props = IEditorProps & {
|
||||
children?: (editor: Editor) => React.ReactNode;
|
||||
extensions: Extension<any, any>[];
|
||||
extensions: AnyExtension[];
|
||||
};
|
||||
|
||||
export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import React from "react";
|
||||
// components
|
||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
||||
import { EditorBubbleMenu } from "@/components/menus";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useCollaborativeRichTextEditor } from "@/hooks/use-collaborative-rich-text-editor";
|
||||
// types
|
||||
import { EditorRefApi, ICollaborativeRichTextEditor } from "@/types";
|
||||
|
||||
const CollaborativeRichTextEditor = (props: ICollaborativeRichTextEditor) => {
|
||||
const {
|
||||
containerClassName,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
id,
|
||||
mentionHandler,
|
||||
onChange,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
} = props;
|
||||
|
||||
const { editor } = useCollaborativeRichTextEditor({
|
||||
editorClassName,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
id,
|
||||
mentionHandler,
|
||||
onChange,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
});
|
||||
|
||||
const editorContainerClassName = getEditorClassNames({
|
||||
noBorder: true,
|
||||
borderOnFocus: false,
|
||||
containerClassName,
|
||||
});
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<EditorContainer
|
||||
displayConfig={displayConfig}
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
>
|
||||
<EditorBubbleMenu editor={editor} />
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const CollaborativeRichTextEditorWithRef = React.forwardRef<EditorRefApi, ICollaborativeRichTextEditor>(
|
||||
(props, ref) => (
|
||||
<CollaborativeRichTextEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
|
||||
)
|
||||
);
|
||||
|
||||
CollaborativeRichTextEditorWithRef.displayName = "CollaborativeRichTextEditorWithRef";
|
||||
|
||||
export { CollaborativeRichTextEditorWithRef };
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import React from "react";
|
||||
// components
|
||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
||||
import { EditorBubbleMenu } from "@/components/menus";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useCollaborativeRichTextReadOnlyEditor } from "@/hooks/use-collaborative-rich-text-read-only-editor";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, ICollaborativeRichTextReadOnlyEditor } from "@/types";
|
||||
|
||||
const CollaborativeRichTextReadOnlyEditor = (props: ICollaborativeRichTextReadOnlyEditor) => {
|
||||
const {
|
||||
containerClassName,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
id,
|
||||
mentionHandler,
|
||||
value,
|
||||
} = props;
|
||||
|
||||
const { editor } = useCollaborativeRichTextReadOnlyEditor({
|
||||
editorClassName,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
id,
|
||||
mentionHandler,
|
||||
value,
|
||||
});
|
||||
|
||||
const editorContainerClassName = getEditorClassNames({
|
||||
noBorder: true,
|
||||
borderOnFocus: false,
|
||||
containerClassName,
|
||||
});
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<EditorContainer
|
||||
displayConfig={displayConfig}
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
>
|
||||
<EditorBubbleMenu editor={editor} />
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper editor={editor} id={id} />
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const CollaborativeRichTextReadOnlyEditorWithRef = React.forwardRef<
|
||||
EditorReadOnlyRefApi,
|
||||
ICollaborativeRichTextReadOnlyEditor
|
||||
>((props, ref) => (
|
||||
<CollaborativeRichTextReadOnlyEditor
|
||||
{...props}
|
||||
forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>}
|
||||
/>
|
||||
));
|
||||
|
||||
CollaborativeRichTextReadOnlyEditorWithRef.displayName = "CollaborativeRichTextReadOnlyEditorWithRef";
|
||||
|
||||
export { CollaborativeRichTextReadOnlyEditorWithRef };
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
export * from "./collaborative-editor";
|
||||
export * from "./collaborative-read-only-editor";
|
||||
export * from "./editor";
|
||||
export * from "./read-only-editor";
|
||||
|
|
|
|||
132
packages/editor/src/core/helpers/yjs-utils.ts
Normal file
132
packages/editor/src/core/helpers/yjs-utils.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@/extensions";
|
||||
import { getSchema } from "@tiptap/core";
|
||||
import { generateHTML, generateJSON } from "@tiptap/html";
|
||||
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
|
||||
// editor extension configs
|
||||
const RICH_TEXT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
|
||||
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
|
||||
// editor schemas
|
||||
const richTextEditorSchema = getSchema(RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
/**
|
||||
* @description apply updates to a doc and return the updated doc in binary format
|
||||
* @param {Uint8Array} document
|
||||
* @param {Uint8Array} updates
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const applyUpdates = (document: Uint8Array, updates?: Uint8Array): Uint8Array => {
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, document);
|
||||
if (updates) {
|
||||
Y.applyUpdate(yDoc, updates);
|
||||
}
|
||||
|
||||
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
|
||||
return encodedDoc;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function encodes binary data to base64 string
|
||||
* @param {Uint8Array} document
|
||||
* @returns {string}
|
||||
*/
|
||||
export const convertBinaryDataToBase64String = (document: Uint8Array): string =>
|
||||
Buffer.from(document).toString("base64");
|
||||
|
||||
/**
|
||||
* @description this function decodes base64 string to binary data
|
||||
* @param {string} document
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
export const convertBase64StringToBinaryData = (document: string): ArrayBuffer => Buffer.from(document, "base64");
|
||||
|
||||
/**
|
||||
* @description this function generates the binary equivalent of html content for the rich text editor
|
||||
* @param {string} descriptionHTML
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
||||
// convert HTML to JSON
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
// convert JSON to Y.Doc format
|
||||
const transformedData = prosemirrorJSONToYDoc(richTextEditorSchema, contentJSON, "default");
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
return encodedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates the binary equivalent of html content for the document editor
|
||||
* @param {string} descriptionHTML
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
||||
// convert HTML to JSON
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", DOCUMENT_EDITOR_EXTENSIONS);
|
||||
// convert JSON to Y.Doc format
|
||||
const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
return encodedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates all document formats for the provided binary data for the rich text editor
|
||||
* @param {Uint8Array} description
|
||||
* @returns
|
||||
*/
|
||||
export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
|
||||
description: Uint8Array
|
||||
): {
|
||||
contentBinaryEncoded: string;
|
||||
contentJSON: object;
|
||||
contentHTML: string;
|
||||
} => {
|
||||
// encode binary description data
|
||||
const base64Data = convertBinaryDataToBase64String(description);
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, description);
|
||||
// convert to JSON
|
||||
const type = yDoc.getXmlFragment("default");
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, richTextEditorSchema).toJSON();
|
||||
// convert to HTML
|
||||
const contentHTML = generateHTML(contentJSON, RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
|
||||
return {
|
||||
contentBinaryEncoded: base64Data,
|
||||
contentJSON,
|
||||
contentHTML,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates all document formats for the provided binary data for the document editor
|
||||
* @param {Uint8Array} description
|
||||
* @returns
|
||||
*/
|
||||
export const getAllDocumentFormatsFromDocumentEditorBinaryData = (
|
||||
description: Uint8Array
|
||||
): {
|
||||
contentBinaryEncoded: string;
|
||||
contentJSON: object;
|
||||
contentHTML: string;
|
||||
} => {
|
||||
// encode binary description data
|
||||
const base64Data = convertBinaryDataToBase64String(description);
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, description);
|
||||
// convert to JSON
|
||||
const type = yDoc.getXmlFragment("default");
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, documentEditorSchema).toJSON();
|
||||
// convert to HTML
|
||||
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
return {
|
||||
contentBinaryEncoded: base64Data,
|
||||
contentJSON,
|
||||
contentHTML,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import * as Y from "yjs";
|
||||
|
||||
/**
|
||||
* @description apply updates to a doc and return the updated doc in base64(binary) format
|
||||
* @param {Uint8Array} document
|
||||
* @param {Uint8Array} updates
|
||||
* @returns {string} base64(binary) form of the updated doc
|
||||
*/
|
||||
export const applyUpdates = (document: Uint8Array, updates: Uint8Array): Uint8Array => {
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, document);
|
||||
Y.applyUpdate(yDoc, updates);
|
||||
|
||||
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
|
||||
return encodedDoc;
|
||||
};
|
||||
|
|
@ -9,9 +9,9 @@ import { useEditor } from "@/hooks/use-editor";
|
|||
// plane editor extensions
|
||||
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||
// types
|
||||
import { TCollaborativeEditorProps } from "@/types";
|
||||
import { TCollaborativeDocumentEditorHookProps } from "@/types";
|
||||
|
||||
export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||
export const useCollaborativeDocumentEditor = (props: TCollaborativeDocumentEditorHookProps) => {
|
||||
const {
|
||||
onTransaction,
|
||||
disabledExtensions,
|
||||
|
|
@ -102,7 +102,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
|||
forwardedRef,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
provider,
|
||||
providerDocument: provider.document,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
|
|
@ -7,9 +7,9 @@ import { HeadingListExtension } from "@/extensions";
|
|||
// hooks
|
||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||
// types
|
||||
import { TReadOnlyCollaborativeEditorProps } from "@/types";
|
||||
import { TCollaborativeDocumentReadOnlyEditorHookProps } from "@/types";
|
||||
|
||||
export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => {
|
||||
export const useCollaborativeDocumentReadOnlyEditor = (props: TCollaborativeDocumentReadOnlyEditorHookProps) => {
|
||||
const {
|
||||
editorClassName,
|
||||
editorProps = {},
|
||||
|
|
@ -79,7 +79,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
|
|||
forwardedRef,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
provider,
|
||||
providerDocument: provider.document,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import Collaboration from "@tiptap/extension-collaboration";
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import { HeadingListExtension, SideMenuExtension } from "@/extensions";
|
||||
// hooks
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
// providers
|
||||
import { CustomCollaborationProvider } from "@/providers";
|
||||
// types
|
||||
import { TCollaborativeRichTextEditorHookProps } from "@/types";
|
||||
|
||||
export const useCollaborativeRichTextEditor = (props: TCollaborativeRichTextEditorHookProps) => {
|
||||
const {
|
||||
editorClassName,
|
||||
editorProps = {},
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
id,
|
||||
mentionHandler,
|
||||
onChange,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
} = props;
|
||||
// initialize custom collaboration provider
|
||||
const provider = useMemo(
|
||||
() =>
|
||||
new CustomCollaborationProvider({
|
||||
name: id,
|
||||
onChange,
|
||||
}),
|
||||
[id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (provider.hasSynced) return;
|
||||
if (value && value.length > 0) {
|
||||
try {
|
||||
Y.applyUpdate(provider.document, value);
|
||||
provider.hasSynced = true;
|
||||
} catch (error) {
|
||||
console.error("Error applying binary updates to the description", error);
|
||||
}
|
||||
}
|
||||
}, [value, provider.document]);
|
||||
|
||||
const editor = useEditor({
|
||||
id,
|
||||
editorProps,
|
||||
editorClassName,
|
||||
enableHistory: false,
|
||||
extensions: [
|
||||
SideMenuExtension({
|
||||
aiEnabled: false,
|
||||
dragDropEnabled: true,
|
||||
}),
|
||||
HeadingListExtension,
|
||||
Collaboration.configure({
|
||||
document: provider.document,
|
||||
}),
|
||||
...(extensions ?? []),
|
||||
],
|
||||
fileHandler,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
providerDocument: provider.document,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
return {
|
||||
editor,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import Collaboration from "@tiptap/extension-collaboration";
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import { HeadingListExtension, SideMenuExtension } from "@/extensions";
|
||||
// hooks
|
||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||
// providers
|
||||
import { CustomCollaborationProvider } from "@/providers";
|
||||
// types
|
||||
import { TCollaborativeRichTextReadOnlyEditorHookProps } from "@/types";
|
||||
|
||||
export const useCollaborativeRichTextReadOnlyEditor = (props: TCollaborativeRichTextReadOnlyEditorHookProps) => {
|
||||
const {
|
||||
editorClassName,
|
||||
editorProps = {},
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
id,
|
||||
mentionHandler,
|
||||
value,
|
||||
} = props;
|
||||
// initialize custom collaboration provider
|
||||
const provider = useMemo(
|
||||
() =>
|
||||
new CustomCollaborationProvider({
|
||||
name: id,
|
||||
}),
|
||||
[id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (value.length > 0) {
|
||||
Y.applyUpdate(provider.document, value);
|
||||
}
|
||||
}, [value, provider.document]);
|
||||
|
||||
const editor = useReadOnlyEditor({
|
||||
editorProps,
|
||||
editorClassName,
|
||||
extensions: [
|
||||
SideMenuExtension({
|
||||
aiEnabled: false,
|
||||
dragDropEnabled: true,
|
||||
}),
|
||||
HeadingListExtension,
|
||||
Collaboration.configure({
|
||||
document: provider.document,
|
||||
}),
|
||||
...(extensions ?? []),
|
||||
],
|
||||
fileHandler,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
providerDocument: provider.document,
|
||||
});
|
||||
|
||||
return {
|
||||
editor,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { DOMSerializer } from "@tiptap/pm/model";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
|
|
@ -36,7 +35,7 @@ export interface CustomEditorProps {
|
|||
onTransaction?: () => void;
|
||||
autofocus?: boolean;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
provider?: HocuspocusProvider;
|
||||
providerDocument?: Y.Doc;
|
||||
tabIndex?: number;
|
||||
// undefined when prop is not passed, null if intentionally passed to stop
|
||||
// swr syncing
|
||||
|
|
@ -58,7 +57,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
onChange,
|
||||
onTransaction,
|
||||
placeholder,
|
||||
provider,
|
||||
providerDocument,
|
||||
tabIndex,
|
||||
value,
|
||||
autofocus = false,
|
||||
|
|
@ -206,7 +205,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
return markdownOutput;
|
||||
},
|
||||
getDocument: () => {
|
||||
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
|
||||
const documentBinary = providerDocument ? Y.encodeStateAsUpdate(providerDocument) : null;
|
||||
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editorRef.current?.getJSON() ?? null;
|
||||
|
||||
|
|
@ -284,7 +283,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
|
||||
}),
|
||||
setProviderDocument: (value) => {
|
||||
const document = provider?.document;
|
||||
const document = providerDocument;
|
||||
if (!document) return;
|
||||
Y.applyUpdate(document, value);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||
import * as Y from "yjs";
|
||||
|
|
@ -24,7 +23,7 @@ interface CustomReadOnlyEditorProps {
|
|||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
};
|
||||
provider?: HocuspocusProvider;
|
||||
providerDocument?: Y.Doc;
|
||||
}
|
||||
|
||||
export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
|
|
@ -37,7 +36,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
|||
fileHandler,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
provider,
|
||||
providerDocument,
|
||||
} = props;
|
||||
|
||||
const editor = useCustomEditor({
|
||||
|
|
@ -86,7 +85,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
|||
return markdownOutput;
|
||||
},
|
||||
getDocument: () => {
|
||||
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
|
||||
const documentBinary = providerDocument ? Y.encodeStateAsUpdate(providerDocument) : null;
|
||||
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editorRef.current?.getJSON() ?? null;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import * as Y from "yjs";
|
||||
|
||||
export interface CompleteCollaborationProviderConfiguration {
|
||||
/**
|
||||
* The identifier/name of your document
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The actual Y.js document
|
||||
*/
|
||||
document: Y.Doc;
|
||||
/**
|
||||
* onChange callback
|
||||
*/
|
||||
onChange: (updates: Uint8Array) => void;
|
||||
}
|
||||
|
||||
export type CollaborationProviderConfiguration = Required<Pick<CompleteCollaborationProviderConfiguration, "name">> &
|
||||
Partial<CompleteCollaborationProviderConfiguration>;
|
||||
|
||||
export class CustomCollaborationProvider {
|
||||
public hasSynced: boolean;
|
||||
|
||||
public configuration: CompleteCollaborationProviderConfiguration = {
|
||||
name: "",
|
||||
document: new Y.Doc(),
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
constructor(configuration: CollaborationProviderConfiguration) {
|
||||
this.hasSynced = false;
|
||||
this.setConfiguration(configuration);
|
||||
this.document.on("update", this.documentUpdateHandler.bind(this));
|
||||
this.document.on("destroy", this.documentDestroyHandler.bind(this));
|
||||
}
|
||||
|
||||
public setConfiguration(configuration: Partial<CompleteCollaborationProviderConfiguration> = {}): void {
|
||||
this.configuration = {
|
||||
...this.configuration,
|
||||
...configuration,
|
||||
};
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.configuration.document;
|
||||
}
|
||||
|
||||
async documentUpdateHandler(_update: Uint8Array, origin: any) {
|
||||
if (!this.hasSynced) return;
|
||||
// return if the update is from the provider itself
|
||||
if (origin === this) return;
|
||||
// call onChange with the update
|
||||
const stateVector = Y.encodeStateAsUpdate(this.document);
|
||||
this.configuration.onChange?.(stateVector);
|
||||
}
|
||||
|
||||
documentDestroyHandler() {
|
||||
this.document.off("update", this.documentUpdateHandler);
|
||||
this.document.off("destroy", this.documentDestroyHandler);
|
||||
}
|
||||
}
|
||||
1
packages/editor/src/core/providers/index.ts
Normal file
1
packages/editor/src/core/providers/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./custom-collaboration-provider";
|
||||
|
|
@ -19,7 +19,7 @@ export type TServerHandler = {
|
|||
onServerError?: () => void;
|
||||
};
|
||||
|
||||
type TCollaborativeEditorHookProps = {
|
||||
type TCollaborativeEditorHookCommonProps = {
|
||||
disabledExtensions?: TExtensions[];
|
||||
editorClassName: string;
|
||||
editorProps?: EditorProps;
|
||||
|
|
@ -30,12 +30,9 @@ type TCollaborativeEditorHookProps = {
|
|||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
};
|
||||
realtimeConfig: TRealtimeConfig;
|
||||
serverHandler?: TServerHandler;
|
||||
user: TUserDetails;
|
||||
};
|
||||
|
||||
export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||
type TCollaborativeEditorHookProps = TCollaborativeEditorHookCommonProps & {
|
||||
onTransaction?: () => void;
|
||||
embedHandler?: TEmbedConfig;
|
||||
fileHandler: TFileHandler;
|
||||
|
|
@ -44,7 +41,29 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
|||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||
type TCollaborativeReadOnlyEditorHookProps = TCollaborativeEditorHookCommonProps & {
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
};
|
||||
|
||||
export type TCollaborativeRichTextEditorHookProps = TCollaborativeEditorHookProps & {
|
||||
onChange: (updatedDescription: Uint8Array) => void;
|
||||
value: Uint8Array;
|
||||
};
|
||||
|
||||
export type TCollaborativeRichTextReadOnlyEditorHookProps = TCollaborativeReadOnlyEditorHookProps & {
|
||||
value: Uint8Array;
|
||||
};
|
||||
|
||||
export type TCollaborativeDocumentEditorHookProps = TCollaborativeEditorHookProps & {
|
||||
embedHandler?: TEmbedConfig;
|
||||
realtimeConfig: TRealtimeConfig;
|
||||
serverHandler?: TServerHandler;
|
||||
user: TUserDetails;
|
||||
};
|
||||
|
||||
export type TCollaborativeDocumentReadOnlyEditorHookProps = TCollaborativeReadOnlyEditorHookProps & {
|
||||
realtimeConfig: TRealtimeConfig;
|
||||
serverHandler?: TServerHandler;
|
||||
user: TUserDetails;
|
||||
};
|
||||
|
|
@ -132,6 +132,12 @@ export interface IRichTextEditor extends IEditorProps {
|
|||
dragDropEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ICollaborativeRichTextEditor extends Omit<IEditorProps, "initialValue" | "onChange" | "value"> {
|
||||
dragDropEnabled?: boolean;
|
||||
onChange: (updatedDescription: Uint8Array) => void;
|
||||
value: Uint8Array;
|
||||
}
|
||||
|
||||
export interface ICollaborativeDocumentEditor
|
||||
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
|
||||
aiHandler?: TAIHandler;
|
||||
|
|
@ -161,6 +167,10 @@ export type ILiteTextReadOnlyEditor = IReadOnlyEditorProps;
|
|||
|
||||
export type IRichTextReadOnlyEditor = IReadOnlyEditorProps;
|
||||
|
||||
export type ICollaborativeRichTextReadOnlyEditor = Omit<IReadOnlyEditorProps, "initialValue"> & {
|
||||
value: Uint8Array;
|
||||
};
|
||||
|
||||
export interface ICollaborativeDocumentReadOnlyEditor extends Omit<IReadOnlyEditorProps, "initialValue"> {
|
||||
embedHandler: TEmbedConfig;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export * from "./ai";
|
||||
export * from "./collaboration";
|
||||
export * from "./collaboration-hook";
|
||||
export * from "./config";
|
||||
export * from "./editor";
|
||||
export * from "./embed";
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import "./styles/drag-drop.css";
|
|||
export {
|
||||
CollaborativeDocumentEditorWithRef,
|
||||
CollaborativeDocumentReadOnlyEditorWithRef,
|
||||
CollaborativeRichTextEditorWithRef,
|
||||
CollaborativeRichTextReadOnlyEditorWithRef,
|
||||
DocumentReadOnlyEditorWithRef,
|
||||
LiteTextEditorWithRef,
|
||||
LiteTextReadOnlyEditorWithRef,
|
||||
|
|
@ -25,7 +27,7 @@ export * from "@/constants/common";
|
|||
// helpers
|
||||
export * from "@/helpers/common";
|
||||
export * from "@/helpers/editor-commands";
|
||||
export * from "@/helpers/yjs";
|
||||
export * from "@/helpers/yjs-utils";
|
||||
export * from "@/extensions/table/table";
|
||||
|
||||
// components
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from "@/extensions/core-without-props";
|
||||
export * from "@/helpers/yjs-utils";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue