mirror of
https://github.com/makeplane/plane.git
synced 2025-12-21 05:10:24 -06:00
chore: sync changes from canary to preview
This commit is contained in:
@@ -20,24 +20,32 @@ const fetchDocument = async ({ context, documentName: pageId, instance }: FetchP
|
||||
try {
|
||||
const service = getPageService(context.documentType, context);
|
||||
// fetch details
|
||||
const response = await service.fetchDescriptionBinary(pageId);
|
||||
const response = (await service.fetchDescriptionBinary(pageId)) as Buffer;
|
||||
const binaryData = new Uint8Array(response);
|
||||
// if binary data is empty, convert HTML to binary data
|
||||
if (binaryData.byteLength === 0) {
|
||||
const pageDetails = await service.fetchDetails(pageId);
|
||||
const convertedBinaryData = getBinaryDataFromDocumentEditorHTMLString(pageDetails.description_html ?? "<p></p>");
|
||||
const convertedBinaryData = getBinaryDataFromDocumentEditorHTMLString(
|
||||
pageDetails.description_html ?? "<p></p>",
|
||||
pageDetails.name
|
||||
);
|
||||
if (convertedBinaryData) {
|
||||
// save the converted binary data back to the database
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromDocumentEditorBinaryData(
|
||||
convertedBinaryData,
|
||||
true
|
||||
);
|
||||
const payload = {
|
||||
description_binary: contentBinaryEncoded,
|
||||
description_html: contentHTML,
|
||||
description: contentJSON,
|
||||
};
|
||||
await service.updateDescriptionBinary(pageId, payload);
|
||||
try {
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromDocumentEditorBinaryData(
|
||||
convertedBinaryData,
|
||||
true
|
||||
);
|
||||
const payload = {
|
||||
description_binary: contentBinaryEncoded,
|
||||
description_html: contentHTML,
|
||||
description: contentJSON,
|
||||
};
|
||||
await service.updateDescriptionBinary(pageId, payload);
|
||||
} catch (e) {
|
||||
const error = new AppError(e);
|
||||
logger.error("Failed to save binary after first convertion from html:", error);
|
||||
}
|
||||
return convertedBinaryData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Database } from "./database";
|
||||
import { ForceCloseHandler } from "./force-close-handler";
|
||||
import { Logger } from "./logger";
|
||||
import { Redis } from "./redis";
|
||||
import { TitleSyncExtension } from "./title-sync";
|
||||
|
||||
export const getExtensions = () => [new Logger(), new Database(), new Redis()];
|
||||
export const getExtensions = () => [
|
||||
new Logger(),
|
||||
new Database(),
|
||||
new Redis(),
|
||||
new TitleSyncExtension(),
|
||||
new ForceCloseHandler(), // Must be after Redis to receive broadcasts
|
||||
];
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
// hocuspocus
|
||||
import type { Extension, Hocuspocus, Document } from "@hocuspocus/server";
|
||||
import { TiptapTransformer } from "@hocuspocus/transformer";
|
||||
import type { AnyExtension, JSONContent } from "@tiptap/core";
|
||||
import type * as Y from "yjs";
|
||||
// editor extensions
|
||||
import { TITLE_EDITOR_EXTENSIONS, createRealtimeEvent } from "@plane/editor";
|
||||
import {
|
||||
TITLE_EDITOR_EXTENSIONS,
|
||||
createRealtimeEvent,
|
||||
extractTextFromHTML,
|
||||
generateTitleProsemirrorJson,
|
||||
} from "@plane/editor";
|
||||
import { logger } from "@plane/logger";
|
||||
import { AppError } from "@/lib/errors";
|
||||
// helpers
|
||||
import { getPageService } from "@/services/page/handler";
|
||||
import type { HocusPocusServerContext, OnLoadDocumentPayloadWithContext } from "@/types";
|
||||
import { generateTitleProsemirrorJson } from "@/utils";
|
||||
import { broadcastMessageToPage } from "@/utils/broadcast-message";
|
||||
import { TitleUpdateManager } from "./title-update/title-update-manager";
|
||||
import { extractTextFromHTML } from "./title-update/title-utils";
|
||||
|
||||
/**
|
||||
* Hocuspocus extension for synchronizing document titles
|
||||
@@ -41,15 +45,11 @@ export class TitleSyncExtension implements Extension {
|
||||
// in the yjs binary
|
||||
if (document.isEmpty("title")) {
|
||||
const service = getPageService(context.documentType, context);
|
||||
// const title = await service.fe
|
||||
const title = (await service.fetchDetails?.(documentName)).name;
|
||||
const pageDetails = await service.fetchDetails(documentName);
|
||||
const title = pageDetails.name;
|
||||
if (title == null) return;
|
||||
const titleField = TiptapTransformer.toYdoc(
|
||||
generateTitleProsemirrorJson(title),
|
||||
"title",
|
||||
// editor
|
||||
TITLE_EDITOR_EXTENSIONS as any
|
||||
);
|
||||
const titleJson = (generateTitleProsemirrorJson as (text: string) => JSONContent)(title);
|
||||
const titleField = TiptapTransformer.toYdoc(titleJson, "title", TITLE_EDITOR_EXTENSIONS as AnyExtension[]);
|
||||
document.merge(titleField);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export const generateTitleProsemirrorJson = (text: string) => {
|
||||
return {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "heading",
|
||||
attrs: { level: 1 },
|
||||
...(text
|
||||
? {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./document";
|
||||
@@ -69,6 +69,7 @@ export const PageRoot = observer(function PageRoot(props: TPageRootProps) {
|
||||
const { isFetchingFallbackBinary } = usePageFallback({
|
||||
editorRef,
|
||||
fetchPageDescription: handlers.fetchDescriptionBinary,
|
||||
page,
|
||||
collaborationState,
|
||||
updatePageDescription: handlers.updateDescription,
|
||||
});
|
||||
|
||||
@@ -2,22 +2,22 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { EditorRefApi, CollaborationState } from "@plane/editor";
|
||||
// plane editor
|
||||
import { convertBinaryDataToBase64String, getBinaryDataFromDocumentEditorHTMLString } from "@plane/editor";
|
||||
// plane propel
|
||||
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
|
||||
// plane types
|
||||
import type { TDocumentPayload } from "@plane/types";
|
||||
// hooks
|
||||
import useAutoSave from "@/hooks/use-auto-save";
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type TArgs = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
fetchPageDescription: () => Promise<ArrayBuffer>;
|
||||
collaborationState: CollaborationState | null;
|
||||
updatePageDescription: (data: TDocumentPayload) => Promise<void>;
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
export const usePageFallback = (args: TArgs) => {
|
||||
const { editorRef, fetchPageDescription, collaborationState, updatePageDescription } = args;
|
||||
const { editorRef, fetchPageDescription, collaborationState, updatePageDescription, page } = args;
|
||||
const hasShownFallbackToast = useRef(false);
|
||||
|
||||
const [isFetchingFallbackBinary, setIsFetchingFallbackBinary] = useState(false);
|
||||
@@ -32,12 +32,7 @@ export const usePageFallback = (args: TArgs) => {
|
||||
|
||||
// Show toast notification when fallback mechanism kicks in (only once)
|
||||
if (!hasShownFallbackToast.current) {
|
||||
// setToast({
|
||||
// type: TOAST_TYPE.WARNING,
|
||||
// title: "Connection lost",
|
||||
// message: "Your changes are being saved using backup mechanism. ",
|
||||
// });
|
||||
console.log("Connection lost");
|
||||
console.warn("Websocket Connection lost, your changes are being saved using backup mechanism.");
|
||||
hasShownFallbackToast.current = true;
|
||||
}
|
||||
|
||||
@@ -49,7 +44,11 @@ export const usePageFallback = (args: TArgs) => {
|
||||
if (latestEncodedDescription && latestEncodedDescription.byteLength > 0) {
|
||||
latestDecodedDescription = new Uint8Array(latestEncodedDescription);
|
||||
} else {
|
||||
latestDecodedDescription = getBinaryDataFromDocumentEditorHTMLString("<p></p>");
|
||||
const pageDescriptionHtml = page.description_html;
|
||||
latestDecodedDescription = getBinaryDataFromDocumentEditorHTMLString(
|
||||
pageDescriptionHtml ?? "<p></p>",
|
||||
page.name
|
||||
);
|
||||
}
|
||||
|
||||
editor.setProviderDocument(latestDecodedDescription);
|
||||
@@ -64,15 +63,10 @@ export const usePageFallback = (args: TArgs) => {
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
// setToast({
|
||||
// type: TOAST_TYPE.ERROR,
|
||||
// title: "Error",
|
||||
// message: `Failed to update description using backup mechanism, ${error?.message}`,
|
||||
// });
|
||||
} finally {
|
||||
setIsFetchingFallbackBinary(false);
|
||||
}
|
||||
}, [editorRef, fetchPageDescription, hasConnectionFailed, updatePageDescription]);
|
||||
}, [editorRef, fetchPageDescription, hasConnectionFailed, updatePageDescription, page.description_html, page.name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasConnectionFailed) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Buffer } from "buffer";
|
||||
import type { Extensions } from "@tiptap/core";
|
||||
import type { Extensions, JSONContent } from "@tiptap/core";
|
||||
import { getSchema } from "@tiptap/core";
|
||||
import { generateHTML, generateJSON } from "@tiptap/html";
|
||||
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
|
||||
@@ -69,16 +69,49 @@ export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: strin
|
||||
return encodedData;
|
||||
};
|
||||
|
||||
export const generateTitleProsemirrorJson = (text: string): JSONContent => {
|
||||
return {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "heading",
|
||||
attrs: { level: 1 },
|
||||
...(text
|
||||
? {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates the binary equivalent of html content for the document editor
|
||||
* @param {string} descriptionHTML
|
||||
* @param {string} descriptionHTML - The HTML content to convert
|
||||
* @param {string} [title] - Optional title to append to the document
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
||||
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string, title?: 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");
|
||||
|
||||
// If title is provided, merge it into the document
|
||||
if (title != null) {
|
||||
const titleJSON = generateTitleProsemirrorJson(title);
|
||||
const titleField = prosemirrorJSONToYDoc(documentEditorSchema, titleJSON, "title");
|
||||
// Encode the title YDoc to updates and apply them to the main document
|
||||
const titleUpdates = Y.encodeStateAsUpdate(titleField);
|
||||
Y.applyUpdate(transformedData, titleUpdates);
|
||||
}
|
||||
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
return encodedData;
|
||||
@@ -207,8 +240,9 @@ export const convertHTMLDocumentToAllFormats = (args: TConvertHTMLDocumentToAllF
|
||||
};
|
||||
|
||||
export const extractTextFromHTML = (html: string): string => {
|
||||
// Use sanitizeHTML to safely extract text and remove all HTML tags
|
||||
// Use DOMPurify to safely extract text and remove all HTML tags
|
||||
// This is more secure than regex as it handles edge cases and prevents injection
|
||||
// Note: sanitizeHTML trims whitespace, which is acceptable for title extraction
|
||||
return sanitizeHTML(html) || "";
|
||||
const sanitizedText = sanitizeHTML(html); // sanitize the string to remove all HTML tags
|
||||
return sanitizedText.trim() || ""; // trim the string to remove leading and trailing whitespaces
|
||||
};
|
||||
|
||||
@@ -187,7 +187,7 @@ export const useYjsSetup = ({ docId, serverUrl, authToken, onStateChange }: UseY
|
||||
|
||||
provider.on("close", handleClose);
|
||||
|
||||
setYjsSession({ provider, ydoc: provider.document });
|
||||
setYjsSession({ provider, ydoc: provider.document as Y.Doc });
|
||||
|
||||
// Handle page visibility changes (sleep/wake, tab switching)
|
||||
const handleVisibilityChange = (event?: Event) => {
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"@plane/types": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "3.2.7",
|
||||
"hast": "^1.0.0",
|
||||
"hast-util-to-mdast": "^10.1.2",
|
||||
"lodash-es": "catalog:",
|
||||
@@ -38,6 +37,7 @@
|
||||
"rehype-remark": "^10.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"sanitize-html": "2.17.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"unified": "^11.0.5",
|
||||
"uuid": "catalog:"
|
||||
@@ -49,6 +49,7 @@
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import type { Content, JSONContent } from "@plane/types";
|
||||
|
||||
/**
|
||||
@@ -120,7 +120,7 @@ const text = stripHTML(html);
|
||||
console.log(text); // Some text
|
||||
*/
|
||||
export const sanitizeHTML = (htmlString: string) => {
|
||||
const sanitizedText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] }); // sanitize the string to remove all HTML tags
|
||||
const sanitizedText = sanitizeHtml(htmlString, { allowedTags: [] }); // sanitize the string to remove all HTML tags
|
||||
return sanitizedText.trim(); // trim the string to remove leading and trailing whitespaces
|
||||
};
|
||||
|
||||
@@ -155,8 +155,8 @@ export const checkEmailValidity = (email: string): boolean => {
|
||||
};
|
||||
|
||||
export const isEmptyHtmlString = (htmlString: string, allowedHTMLTags: string[] = []) => {
|
||||
// Remove HTML tags using DOMPurify
|
||||
const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: allowedHTMLTags });
|
||||
// Remove HTML tags using sanitize-html
|
||||
const cleanText = sanitizeHtml(htmlString, { allowedTags: allowedHTMLTags });
|
||||
// Trim the string and check if it's empty
|
||||
return cleanText.trim() === "";
|
||||
};
|
||||
|
||||
544
pnpm-lock.yaml
generated
544
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user