Skip to content

Commit f5fe8d0

Browse files
chore: sync changes from canary to preview
2 parents 0370a1b + e0c97c5 commit f5fe8d0

File tree

12 files changed

+588
-126
lines changed

12 files changed

+588
-126
lines changed

apps/live/src/extensions/database.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,32 @@ const fetchDocument = async ({ context, documentName: pageId, instance }: FetchP
2020
try {
2121
const service = getPageService(context.documentType, context);
2222
// fetch details
23-
const response = await service.fetchDescriptionBinary(pageId);
23+
const response = (await service.fetchDescriptionBinary(pageId)) as Buffer;
2424
const binaryData = new Uint8Array(response);
2525
// if binary data is empty, convert HTML to binary data
2626
if (binaryData.byteLength === 0) {
2727
const pageDetails = await service.fetchDetails(pageId);
28-
const convertedBinaryData = getBinaryDataFromDocumentEditorHTMLString(pageDetails.description_html ?? "<p></p>");
28+
const convertedBinaryData = getBinaryDataFromDocumentEditorHTMLString(
29+
pageDetails.description_html ?? "<p></p>",
30+
pageDetails.name
31+
);
2932
if (convertedBinaryData) {
3033
// save the converted binary data back to the database
31-
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromDocumentEditorBinaryData(
32-
convertedBinaryData,
33-
true
34-
);
35-
const payload = {
36-
description_binary: contentBinaryEncoded,
37-
description_html: contentHTML,
38-
description: contentJSON,
39-
};
40-
await service.updateDescriptionBinary(pageId, payload);
34+
try {
35+
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromDocumentEditorBinaryData(
36+
convertedBinaryData,
37+
true
38+
);
39+
const payload = {
40+
description_binary: contentBinaryEncoded,
41+
description_html: contentHTML,
42+
description: contentJSON,
43+
};
44+
await service.updateDescriptionBinary(pageId, payload);
45+
} catch (e) {
46+
const error = new AppError(e);
47+
logger.error("Failed to save binary after first convertion from html:", error);
48+
}
4149
return convertedBinaryData;
4250
}
4351
}

apps/live/src/extensions/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { Database } from "./database";
2+
import { ForceCloseHandler } from "./force-close-handler";
23
import { Logger } from "./logger";
34
import { Redis } from "./redis";
5+
import { TitleSyncExtension } from "./title-sync";
46

5-
export const getExtensions = () => [new Logger(), new Database(), new Redis()];
7+
export const getExtensions = () => [
8+
new Logger(),
9+
new Database(),
10+
new Redis(),
11+
new TitleSyncExtension(),
12+
new ForceCloseHandler(), // Must be after Redis to receive broadcasts
13+
];

apps/live/src/extensions/title-sync.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
// hocuspocus
22
import type { Extension, Hocuspocus, Document } from "@hocuspocus/server";
33
import { TiptapTransformer } from "@hocuspocus/transformer";
4+
import type { AnyExtension, JSONContent } from "@tiptap/core";
45
import type * as Y from "yjs";
56
// editor extensions
6-
import { TITLE_EDITOR_EXTENSIONS, createRealtimeEvent } from "@plane/editor";
7+
import {
8+
TITLE_EDITOR_EXTENSIONS,
9+
createRealtimeEvent,
10+
extractTextFromHTML,
11+
generateTitleProsemirrorJson,
12+
} from "@plane/editor";
713
import { logger } from "@plane/logger";
814
import { AppError } from "@/lib/errors";
915
// helpers
1016
import { getPageService } from "@/services/page/handler";
1117
import type { HocusPocusServerContext, OnLoadDocumentPayloadWithContext } from "@/types";
12-
import { generateTitleProsemirrorJson } from "@/utils";
1318
import { broadcastMessageToPage } from "@/utils/broadcast-message";
1419
import { TitleUpdateManager } from "./title-update/title-update-manager";
15-
import { extractTextFromHTML } from "./title-update/title-utils";
1620

1721
/**
1822
* Hocuspocus extension for synchronizing document titles
@@ -41,15 +45,11 @@ export class TitleSyncExtension implements Extension {
4145
// in the yjs binary
4246
if (document.isEmpty("title")) {
4347
const service = getPageService(context.documentType, context);
44-
// const title = await service.fe
45-
const title = (await service.fetchDetails?.(documentName)).name;
48+
const pageDetails = await service.fetchDetails(documentName);
49+
const title = pageDetails.name;
4650
if (title == null) return;
47-
const titleField = TiptapTransformer.toYdoc(
48-
generateTitleProsemirrorJson(title),
49-
"title",
50-
// editor
51-
TITLE_EDITOR_EXTENSIONS as any
52-
);
51+
const titleJson = (generateTitleProsemirrorJson as (text: string) => JSONContent)(title);
52+
const titleField = TiptapTransformer.toYdoc(titleJson, "title", TITLE_EDITOR_EXTENSIONS as AnyExtension[]);
5353
document.merge(titleField);
5454
}
5555
} catch (error) {

apps/live/src/utils/document.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

apps/live/src/utils/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/web/core/components/pages/editor/page-root.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const PageRoot = observer(function PageRoot(props: TPageRootProps) {
6969
const { isFetchingFallbackBinary } = usePageFallback({
7070
editorRef,
7171
fetchPageDescription: handlers.fetchDescriptionBinary,
72+
page,
7273
collaborationState,
7374
updatePageDescription: handlers.updateDescription,
7475
});

apps/web/core/hooks/use-page-fallback.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@ import { useCallback, useEffect, useRef, useState } from "react";
22
import type { EditorRefApi, CollaborationState } from "@plane/editor";
33
// plane editor
44
import { convertBinaryDataToBase64String, getBinaryDataFromDocumentEditorHTMLString } from "@plane/editor";
5-
// plane propel
6-
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
75
// plane types
86
import type { TDocumentPayload } from "@plane/types";
97
// hooks
108
import useAutoSave from "@/hooks/use-auto-save";
9+
import type { TPageInstance } from "@/store/pages/base-page";
1110

1211
type TArgs = {
1312
editorRef: React.RefObject<EditorRefApi>;
1413
fetchPageDescription: () => Promise<ArrayBuffer>;
1514
collaborationState: CollaborationState | null;
1615
updatePageDescription: (data: TDocumentPayload) => Promise<void>;
16+
page: TPageInstance;
1717
};
1818

1919
export const usePageFallback = (args: TArgs) => {
20-
const { editorRef, fetchPageDescription, collaborationState, updatePageDescription } = args;
20+
const { editorRef, fetchPageDescription, collaborationState, updatePageDescription, page } = args;
2121
const hasShownFallbackToast = useRef(false);
2222

2323
const [isFetchingFallbackBinary, setIsFetchingFallbackBinary] = useState(false);
@@ -32,12 +32,7 @@ export const usePageFallback = (args: TArgs) => {
3232

3333
// Show toast notification when fallback mechanism kicks in (only once)
3434
if (!hasShownFallbackToast.current) {
35-
// setToast({
36-
// type: TOAST_TYPE.WARNING,
37-
// title: "Connection lost",
38-
// message: "Your changes are being saved using backup mechanism. ",
39-
// });
40-
console.log("Connection lost");
35+
console.warn("Websocket Connection lost, your changes are being saved using backup mechanism.");
4136
hasShownFallbackToast.current = true;
4237
}
4338

@@ -49,7 +44,11 @@ export const usePageFallback = (args: TArgs) => {
4944
if (latestEncodedDescription && latestEncodedDescription.byteLength > 0) {
5045
latestDecodedDescription = new Uint8Array(latestEncodedDescription);
5146
} else {
52-
latestDecodedDescription = getBinaryDataFromDocumentEditorHTMLString("<p></p>");
47+
const pageDescriptionHtml = page.description_html;
48+
latestDecodedDescription = getBinaryDataFromDocumentEditorHTMLString(
49+
pageDescriptionHtml ?? "<p></p>",
50+
page.name
51+
);
5352
}
5453

5554
editor.setProviderDocument(latestDecodedDescription);
@@ -64,15 +63,10 @@ export const usePageFallback = (args: TArgs) => {
6463
});
6564
} catch (error: any) {
6665
console.error(error);
67-
// setToast({
68-
// type: TOAST_TYPE.ERROR,
69-
// title: "Error",
70-
// message: `Failed to update description using backup mechanism, ${error?.message}`,
71-
// });
7266
} finally {
7367
setIsFetchingFallbackBinary(false);
7468
}
75-
}, [editorRef, fetchPageDescription, hasConnectionFailed, updatePageDescription]);
69+
}, [editorRef, fetchPageDescription, hasConnectionFailed, updatePageDescription, page.description_html, page.name]);
7670

7771
useEffect(() => {
7872
if (hasConnectionFailed) {

packages/editor/src/core/helpers/yjs-utils.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Buffer } from "buffer";
2-
import type { Extensions } from "@tiptap/core";
2+
import type { Extensions, JSONContent } from "@tiptap/core";
33
import { getSchema } from "@tiptap/core";
44
import { generateHTML, generateJSON } from "@tiptap/html";
55
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
@@ -69,16 +69,49 @@ export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: strin
6969
return encodedData;
7070
};
7171

72+
export const generateTitleProsemirrorJson = (text: string): JSONContent => {
73+
return {
74+
type: "doc",
75+
content: [
76+
{
77+
type: "heading",
78+
attrs: { level: 1 },
79+
...(text
80+
? {
81+
content: [
82+
{
83+
type: "text",
84+
text,
85+
},
86+
],
87+
}
88+
: {}),
89+
},
90+
],
91+
};
92+
};
93+
7294
/**
7395
* @description this function generates the binary equivalent of html content for the document editor
74-
* @param {string} descriptionHTML
96+
* @param {string} descriptionHTML - The HTML content to convert
97+
* @param {string} [title] - Optional title to append to the document
7598
* @returns {Uint8Array}
7699
*/
77-
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string): Uint8Array => {
100+
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string, title?: string): Uint8Array => {
78101
// convert HTML to JSON
79102
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", DOCUMENT_EDITOR_EXTENSIONS);
80103
// convert JSON to Y.Doc format
81104
const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
105+
106+
// If title is provided, merge it into the document
107+
if (title != null) {
108+
const titleJSON = generateTitleProsemirrorJson(title);
109+
const titleField = prosemirrorJSONToYDoc(documentEditorSchema, titleJSON, "title");
110+
// Encode the title YDoc to updates and apply them to the main document
111+
const titleUpdates = Y.encodeStateAsUpdate(titleField);
112+
Y.applyUpdate(transformedData, titleUpdates);
113+
}
114+
82115
// convert Y.Doc to Uint8Array format
83116
const encodedData = Y.encodeStateAsUpdate(transformedData);
84117
return encodedData;
@@ -207,8 +240,9 @@ export const convertHTMLDocumentToAllFormats = (args: TConvertHTMLDocumentToAllF
207240
};
208241

209242
export const extractTextFromHTML = (html: string): string => {
210-
// Use sanitizeHTML to safely extract text and remove all HTML tags
243+
// Use DOMPurify to safely extract text and remove all HTML tags
211244
// This is more secure than regex as it handles edge cases and prevents injection
212245
// Note: sanitizeHTML trims whitespace, which is acceptable for title extraction
213-
return sanitizeHTML(html) || "";
246+
const sanitizedText = sanitizeHTML(html); // sanitize the string to remove all HTML tags
247+
return sanitizedText.trim() || ""; // trim the string to remove leading and trailing whitespaces
214248
};

packages/editor/src/core/hooks/use-yjs-setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export const useYjsSetup = ({ docId, serverUrl, authToken, onStateChange }: UseY
187187

188188
provider.on("close", handleClose);
189189

190-
setYjsSession({ provider, ydoc: provider.document });
190+
setYjsSession({ provider, ydoc: provider.document as Y.Doc });
191191

192192
// Handle page visibility changes (sleep/wake, tab switching)
193193
const handleVisibilityChange = (event?: Event) => {

packages/utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"@plane/types": "workspace:*",
2828
"clsx": "^2.1.1",
2929
"date-fns": "^4.1.0",
30-
"dompurify": "3.2.7",
3130
"hast": "^1.0.0",
3231
"hast-util-to-mdast": "^10.1.2",
3332
"lodash-es": "catalog:",
@@ -38,6 +37,7 @@
3837
"rehype-remark": "^10.0.1",
3938
"remark-gfm": "^4.0.1",
4039
"remark-stringify": "^11.0.0",
40+
"sanitize-html": "2.17.0",
4141
"tailwind-merge": "^2.5.5",
4242
"unified": "^11.0.5",
4343
"uuid": "catalog:"
@@ -49,6 +49,7 @@
4949
"@types/mdast": "^4.0.4",
5050
"@types/node": "catalog:",
5151
"@types/react": "catalog:",
52+
"@types/sanitize-html": "2.16.0",
5253
"tsdown": "catalog:",
5354
"typescript": "catalog:"
5455
},

0 commit comments

Comments
 (0)