regression: downgrade to tiptap v2 (#7982)

* chore: downgrade to tiptap v2

* fix: revert back to hocuspocus

* fix: collaboration events added

* fix: lock unlock issues

* fix: build errors

* fix: type errors

* fix: graceful shutdown

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2025-10-21 18:28:16 +05:30 committed by GitHub
parent 59022b6beb
commit 64781be7d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 2123 additions and 824 deletions

View file

@ -0,0 +1,38 @@
import { type Hocuspocus } from "@hocuspocus/server";
import { createRealtimeEvent } from "@plane/editor";
import { logger } from "@plane/logger";
import type { FetchPayloadWithContext, StorePayloadWithContext } from "@/types";
import { broadcastMessageToPage } from "./broadcast-message";
// Helper to broadcast error to frontend
export const broadcastError = async (
hocuspocusServerInstance: Hocuspocus,
pageId: string,
errorMessage: string,
errorType: "fetch" | "store",
context: FetchPayloadWithContext["context"] | StorePayloadWithContext["context"],
errorCode?: "content_too_large" | "page_locked" | "page_archived",
shouldDisconnect?: boolean
) => {
try {
const errorEvent = createRealtimeEvent({
action: "error",
page_id: pageId,
parent_id: undefined,
descendants_ids: [],
data: {
error_message: errorMessage,
error_type: errorType,
error_code: errorCode,
should_disconnect: shouldDisconnect,
user_id: context.userId || "",
},
workspace_slug: context.workspaceSlug || "",
user_id: context.userId || "",
});
await broadcastMessageToPage(hocuspocusServerInstance, pageId, errorEvent);
} catch (broadcastError) {
logger.error("Error broadcasting error message to frontend:", broadcastError);
}
};

View file

@ -0,0 +1,34 @@
import { Hocuspocus } from "@hocuspocus/server";
import { BroadcastedEvent } from "@plane/editor";
import { logger } from "@plane/logger";
import { Redis } from "@/extensions/redis";
import { AppError } from "@/lib/errors";
export const broadcastMessageToPage = async (
hocuspocusServerInstance: Hocuspocus,
documentName: string,
eventData: BroadcastedEvent
): Promise<boolean> => {
if (!hocuspocusServerInstance || !hocuspocusServerInstance.documents) {
const appError = new AppError("HocusPocus server not available or initialized", {
context: { operation: "broadcastMessageToPage", documentName },
});
logger.error("Error while broadcasting message:", appError);
return false;
}
const redisExtension = hocuspocusServerInstance.configuration.extensions.find((ext) => ext instanceof Redis);
if (!redisExtension) {
logger.error("BROADCAST_MESSAGE_TO_PAGE: Redis extension not found");
return false;
}
try {
await redisExtension.broadcastToDocument(documentName, eventData);
return true;
} catch (error) {
logger.error(`BROADCAST_MESSAGE_TO_PAGE: Error broadcasting to ${documentName}:`, error);
return false;
}
};

View file

@ -1,83 +1,21 @@
import { getSchema } from "@tiptap/core";
import { generateHTML, generateJSON } from "@tiptap/html";
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
import * as Y from "yjs";
// plane editor
import {
getAllDocumentFormatsFromDocumentEditorBinaryData,
getAllDocumentFormatsFromRichTextEditorBinaryData,
getBinaryDataFromDocumentEditorHTMLString,
getBinaryDataFromRichTextEditorHTMLString,
} from "@plane/editor";
import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@plane/editor/lib";
// plane types
import { TDocumentPayload } from "@plane/types";
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
type TArgs = {
document_html: string;
variant: "rich" | "document";
};
export const convertHTMLDocumentToAllFormats = (args: TArgs): TDocumentPayload => {
const { document_html, variant } = args;
if (variant === "rich") {
const contentBinary = getBinaryDataFromRichTextEditorHTMLString(document_html);
const { contentBinaryEncoded, contentHTML, contentJSON } =
getAllDocumentFormatsFromRichTextEditorBinaryData(contentBinary);
return {
description: contentJSON,
description_html: contentHTML,
description_binary: contentBinaryEncoded,
};
}
if (variant === "document") {
const contentBinary = getBinaryDataFromDocumentEditorHTMLString(document_html);
const { contentBinaryEncoded, contentHTML, contentJSON } =
getAllDocumentFormatsFromDocumentEditorBinaryData(contentBinary);
return {
description: contentJSON,
description_html: contentHTML,
description_binary: contentBinaryEncoded,
};
}
throw new Error(`Invalid variant provided: ${variant}`);
};
export const getAllDocumentFormatsFromBinaryData = (
description: Uint8Array
): {
contentBinaryEncoded: string;
contentJSON: object;
contentHTML: string;
} => {
// encode binary description data
const base64Data = Buffer.from(description).toString("base64");
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);
export const generateTitleProsemirrorJson = (text: string) => {
return {
contentBinaryEncoded: base64Data,
contentJSON,
contentHTML,
type: "doc",
content: [
{
type: "heading",
attrs: { level: 1 },
...(text
? {
content: [
{
type: "text",
text,
},
],
}
: {}),
},
],
};
};
export const getBinaryDataFromHTMLString = (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
return Y.encodeStateAsUpdate(transformedData);
};