[WEB-1116] refactor: page helpers for document transformation (#5503)

* refactor: page helpers for document transformation

* refactor: update tranforamtion function name
This commit is contained in:
Aaryan Khandelwal 2024-09-03 15:31:32 +05:30 committed by GitHub
parent 539acd58f7
commit 9910ed6e5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 109 additions and 66 deletions

View file

@ -36,9 +36,9 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/express-ws": "^3.0.4", "@types/express-ws": "^3.0.4",
"@types/node": "^20.14.9", "@types/node": "^20.14.9",
"tsup": "^7.2.0",
"nodemon": "^3.1.0", "nodemon": "^3.1.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsup": "^7.2.0",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }

View file

@ -0,0 +1,59 @@
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 { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@plane/editor/lib";
const DOCUMENT_EDITOR_EXTENSIONS = [
...CoreEditorExtensionsWithoutProps,
...DocumentEditorExtensionsWithoutProps,
];
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
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);
return {
contentBinaryEncoded: base64Data,
contentJSON,
contentHTML,
};
}
export const getBinaryDataFromHTMLString = (descriptionHTML: string): {
contentBinary: 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 {
contentBinary: encodedData
}
}

View file

@ -39,14 +39,15 @@ export const handleAuthentication = async (props: Props) => {
"Authentication failed: Incomplete query params. Either workspaceSlug or projectId is missing." "Authentication failed: Incomplete query params. Either workspaceSlug or projectId is missing."
); );
} }
// fetch current user's roles // fetch current user's project membership info
const workspaceRoles = await userService.getUserAllProjectsRole( const projectMembershipInfo = await userService.getUserProjectMembership(
workspaceSlug, workspaceSlug,
projectId,
cookie cookie
); );
const currentProjectRole = workspaceRoles[projectId]; const projectRole = projectMembershipInfo.role;
// make the connection read only for roles lower than a member // make the connection read only for roles lower than a member
if (currentProjectRole < 15) { if (projectRole < 15) {
connection.readOnly = true; connection.readOnly = true;
} }
} else { } else {

View file

@ -1,25 +1,9 @@
import { getSchema } from "@tiptap/core"; // helpers
import { generateHTML, generateJSON } from "@tiptap/html"; import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "../../core/helpers/page.js";
import * as Y from "yjs";
import {
prosemirrorJSONToYDoc,
yXmlFragmentToProseMirrorRootNode,
} from "y-prosemirror";
// editor
import {
CoreEditorExtensionsWithoutProps,
DocumentEditorExtensionsWithoutProps,
} from "@plane/editor/lib";
// services // services
import { PageService } from "../services/page.service.js"; import { PageService } from "../services/page.service.js";
const pageService = new PageService(); const pageService = new PageService();
const DOCUMENT_EDITOR_EXTENSIONS = [
...CoreEditorExtensionsWithoutProps,
...DocumentEditorExtensionsWithoutProps,
];
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
export const updatePageDescription = async ( export const updatePageDescription = async (
params: URLSearchParams, params: URLSearchParams,
pageId: string, pageId: string,
@ -35,22 +19,15 @@ export const updatePageDescription = async (
const workspaceSlug = params.get("workspaceSlug")?.toString(); const workspaceSlug = params.get("workspaceSlug")?.toString();
const projectId = params.get("projectId")?.toString(); const projectId = params.get("projectId")?.toString();
if (!workspaceSlug || !projectId || !cookie) return; if (!workspaceSlug || !projectId || !cookie) return;
// encode binary description data
const base64Data = Buffer.from(updatedDescription).toString("base64");
const yDoc = new Y.Doc();
Y.applyUpdate(yDoc, updatedDescription);
// convert to JSON
const type = yDoc.getXmlFragment("default");
const contentJSON = yXmlFragmentToProseMirrorRootNode(
type,
documentEditorSchema
).toJSON();
// convert to HTML
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
const {
contentBinaryEncoded,
contentHTML,
contentJSON
} = getAllDocumentFormatsFromBinaryData(updatedDescription);
try { try {
const payload = { const payload = {
description_binary: base64Data, description_binary: contentBinaryEncoded,
description_html: contentHTML, description_html: contentHTML,
description: contentJSON, description: contentJSON,
}; };
@ -83,23 +60,8 @@ const fetchDescriptionHTMLAndTransform = async (
pageId, pageId,
cookie cookie
); );
// convert already existing html to json const { contentBinary } = getBinaryDataFromHTMLString(pageDetails.description_html ?? "<p></p>")
const contentJSON = generateJSON( return contentBinary;
pageDetails.description_html ?? "<p></p>",
DOCUMENT_EDITOR_EXTENSIONS
);
// get editor schema from the DOCUMENT_EDITOR_EXTENSIONS array
const schema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
// convert json to Y.Doc format
const transformedData = prosemirrorJSONToYDoc(
schema,
contentJSON,
"default"
);
// convert Y.Doc to Uint8Array format
const encodedData = Y.encodeStateAsUpdate(transformedData);
return encodedData;
} catch (error) { } catch (error) {
console.error("Error while transforming from HTML to Uint8Array", error); console.error("Error while transforming from HTML to Uint8Array", error);
throw error; throw error;

View file

@ -1,5 +1,5 @@
// types // types
import type { IUser, IUserProjectsRole } from "@plane/types"; import type { IProjectMember, IUser } from "@plane/types";
// services // services
import { API_BASE_URL, APIService } from "./api.service.js"; import { API_BASE_URL, APIService } from "./api.service.js";
@ -26,21 +26,36 @@ export class UserService extends APIService {
}); });
} }
async getUserAllProjectsRole( async getUserWorkspaceMembership(
workspaceSlug: string, workspaceSlug: string,
cookie: string cookie: string
): Promise<IUserProjectsRole> { ): Promise<IProjectMember> {
return this.get( return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`,
`/api/users/me/workspaces/${workspaceSlug}/project-roles/`,
{ {
headers: { headers: {
Cookie: cookie, Cookie: cookie,
}, },
} })
)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response;
});
}
async getUserProjectMembership(
workspaceSlug: string,
projectId: string,
cookie: string
): Promise<IProjectMember> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`,
{
headers: {
Cookie: cookie,
},
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
}); });
} }
} }

View file

@ -1,14 +1,17 @@
import { HocuspocusProvider } from "@hocuspocus/provider";
import { Extensions } from "@tiptap/core"; import { Extensions } from "@tiptap/core";
import { SlashCommand } from "@/extensions"; import { SlashCommand } from "@/extensions";
// plane editor types // plane editor types
import { TIssueEmbedConfig } from "@/plane-editor/types"; import { TIssueEmbedConfig } from "@/plane-editor/types";
// types // types
import { TExtensions, TFileHandler } from "@/types"; import { TExtensions, TFileHandler, TUserDetails } from "@/types";
type Props = { type Props = {
disabledExtensions?: TExtensions[]; disabledExtensions?: TExtensions[];
fileHandler: TFileHandler; fileHandler: TFileHandler;
issueEmbedConfig: TIssueEmbedConfig | undefined; issueEmbedConfig: TIssueEmbedConfig | undefined;
provider: HocuspocusProvider;
userDetails: TUserDetails;
}; };
export const DocumentEditorAdditionalExtensions = (props: Props) => { export const DocumentEditorAdditionalExtensions = (props: Props) => {

View file

@ -2,13 +2,14 @@ import { useEffect, useLayoutEffect, useMemo } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider"; import { HocuspocusProvider } from "@hocuspocus/provider";
import Collaboration from "@tiptap/extension-collaboration"; import Collaboration from "@tiptap/extension-collaboration";
import { IndexeddbPersistence } from "y-indexeddb"; import { IndexeddbPersistence } from "y-indexeddb";
// extensions
import { SideMenuExtension } from "@/extensions";
// hooks // hooks
import { useEditor } from "@/hooks/use-editor"; import { useEditor } from "@/hooks/use-editor";
// plane editor extensions // plane editor extensions
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions"; import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types // types
import { TCollaborativeEditorProps } from "@/types"; import { TCollaborativeEditorProps } from "@/types";
import { SideMenuExtension } from "@/extensions";
export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
const { const {
@ -84,6 +85,8 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
disabledExtensions, disabledExtensions,
fileHandler, fileHandler,
issueEmbedConfig: embedHandler?.issue, issueEmbedConfig: embedHandler?.issue,
provider,
userDetails: user,
}), }),
], ],
placeholder, placeholder,

View file

@ -1 +1 @@
export type TExtensions = "ai" | "issue-embed"; export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed";

View file

@ -8,6 +8,6 @@ export const useEditorFlagging = (): {
documentEditor: TExtensions[]; documentEditor: TExtensions[];
richTextEditor: TExtensions[]; richTextEditor: TExtensions[];
} => ({ } => ({
documentEditor: ["ai"], documentEditor: ["ai", "collaboration-cursor"],
richTextEditor: ["ai"], richTextEditor: ["ai", "collaboration-cursor"],
}); });