diff --git a/live/package.json b/live/package.json index 91cad6797..7c4b3956a 100644 --- a/live/package.json +++ b/live/package.json @@ -36,9 +36,9 @@ "@types/express": "^4.17.21", "@types/express-ws": "^3.0.4", "@types/node": "^20.14.9", - "tsup": "^7.2.0", "nodemon": "^3.1.0", "ts-node": "^10.9.2", + "tsup": "^7.2.0", "typescript": "^5.4.5" } } diff --git a/live/src/core/helpers/page.ts b/live/src/core/helpers/page.ts new file mode 100644 index 000000000..4e79afe6b --- /dev/null +++ b/live/src/core/helpers/page.ts @@ -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 ?? "

", + 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 + } +} \ No newline at end of file diff --git a/live/src/core/lib/authentication.ts b/live/src/core/lib/authentication.ts index 9a746b8f0..222871238 100644 --- a/live/src/core/lib/authentication.ts +++ b/live/src/core/lib/authentication.ts @@ -39,14 +39,15 @@ export const handleAuthentication = async (props: Props) => { "Authentication failed: Incomplete query params. Either workspaceSlug or projectId is missing." ); } - // fetch current user's roles - const workspaceRoles = await userService.getUserAllProjectsRole( + // fetch current user's project membership info + const projectMembershipInfo = await userService.getUserProjectMembership( workspaceSlug, + projectId, cookie ); - const currentProjectRole = workspaceRoles[projectId]; + const projectRole = projectMembershipInfo.role; // make the connection read only for roles lower than a member - if (currentProjectRole < 15) { + if (projectRole < 15) { connection.readOnly = true; } } else { diff --git a/live/src/core/lib/page.ts b/live/src/core/lib/page.ts index b9c206bad..30cf10f41 100644 --- a/live/src/core/lib/page.ts +++ b/live/src/core/lib/page.ts @@ -1,25 +1,9 @@ -import { getSchema } from "@tiptap/core"; -import { generateHTML, generateJSON } from "@tiptap/html"; -import * as Y from "yjs"; -import { - prosemirrorJSONToYDoc, - yXmlFragmentToProseMirrorRootNode, -} from "y-prosemirror"; -// editor -import { - CoreEditorExtensionsWithoutProps, - DocumentEditorExtensionsWithoutProps, -} from "@plane/editor/lib"; +// helpers +import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "../../core/helpers/page.js"; // services import { PageService } from "../services/page.service.js"; const pageService = new PageService(); -const DOCUMENT_EDITOR_EXTENSIONS = [ - ...CoreEditorExtensionsWithoutProps, - ...DocumentEditorExtensionsWithoutProps, -]; -const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS); - export const updatePageDescription = async ( params: URLSearchParams, pageId: string, @@ -35,22 +19,15 @@ export const updatePageDescription = async ( const workspaceSlug = params.get("workspaceSlug")?.toString(); const projectId = params.get("projectId")?.toString(); 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 { const payload = { - description_binary: base64Data, + description_binary: contentBinaryEncoded, description_html: contentHTML, description: contentJSON, }; @@ -83,23 +60,8 @@ const fetchDescriptionHTMLAndTransform = async ( pageId, cookie ); - // convert already existing html to json - const contentJSON = generateJSON( - pageDetails.description_html ?? "

", - 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; + const { contentBinary } = getBinaryDataFromHTMLString(pageDetails.description_html ?? "

") + return contentBinary; } catch (error) { console.error("Error while transforming from HTML to Uint8Array", error); throw error; diff --git a/live/src/core/services/user.service.ts b/live/src/core/services/user.service.ts index 96b796c94..9afc447d2 100644 --- a/live/src/core/services/user.service.ts +++ b/live/src/core/services/user.service.ts @@ -1,5 +1,5 @@ // types -import type { IUser, IUserProjectsRole } from "@plane/types"; +import type { IProjectMember, IUser } from "@plane/types"; // services import { API_BASE_URL, APIService } from "./api.service.js"; @@ -26,21 +26,36 @@ export class UserService extends APIService { }); } - async getUserAllProjectsRole( + async getUserWorkspaceMembership( workspaceSlug: string, cookie: string - ): Promise { - return this.get( - `/api/users/me/workspaces/${workspaceSlug}/project-roles/`, + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`, { headers: { Cookie: cookie, }, - } - ) + }) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + throw error?.response; + }); + } + + async getUserProjectMembership( + workspaceSlug: string, + projectId: string, + cookie: string + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`, + { + headers: { + Cookie: cookie, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; }); } } diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index 99631a198..bf2937fcd 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -1,14 +1,17 @@ +import { HocuspocusProvider } from "@hocuspocus/provider"; import { Extensions } from "@tiptap/core"; import { SlashCommand } from "@/extensions"; // plane editor types import { TIssueEmbedConfig } from "@/plane-editor/types"; // types -import { TExtensions, TFileHandler } from "@/types"; +import { TExtensions, TFileHandler, TUserDetails } from "@/types"; type Props = { disabledExtensions?: TExtensions[]; fileHandler: TFileHandler; issueEmbedConfig: TIssueEmbedConfig | undefined; + provider: HocuspocusProvider; + userDetails: TUserDetails; }; export const DocumentEditorAdditionalExtensions = (props: Props) => { diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 4c03c9ed5..eba56b099 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -2,13 +2,14 @@ import { useEffect, useLayoutEffect, useMemo } from "react"; import { HocuspocusProvider } from "@hocuspocus/provider"; import Collaboration from "@tiptap/extension-collaboration"; import { IndexeddbPersistence } from "y-indexeddb"; +// extensions +import { SideMenuExtension } from "@/extensions"; // hooks import { useEditor } from "@/hooks/use-editor"; // plane editor extensions import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types import { TCollaborativeEditorProps } from "@/types"; -import { SideMenuExtension } from "@/extensions"; export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { const { @@ -84,6 +85,8 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { disabledExtensions, fileHandler, issueEmbedConfig: embedHandler?.issue, + provider, + userDetails: user, }), ], placeholder, diff --git a/packages/editor/src/core/types/extensions.ts b/packages/editor/src/core/types/extensions.ts index 4f86b4120..da8713f10 100644 --- a/packages/editor/src/core/types/extensions.ts +++ b/packages/editor/src/core/types/extensions.ts @@ -1 +1 @@ -export type TExtensions = "ai" | "issue-embed"; +export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed"; diff --git a/web/ce/hooks/use-editor-flagging.ts b/web/ce/hooks/use-editor-flagging.ts index 9077d216e..ddaee7165 100644 --- a/web/ce/hooks/use-editor-flagging.ts +++ b/web/ce/hooks/use-editor-flagging.ts @@ -8,6 +8,6 @@ export const useEditorFlagging = (): { documentEditor: TExtensions[]; richTextEditor: TExtensions[]; } => ({ - documentEditor: ["ai"], - richTextEditor: ["ai"], + documentEditor: ["ai", "collaboration-cursor"], + richTextEditor: ["ai", "collaboration-cursor"], });