[WEB-1116] chore: add fallback for the live server (#5622)

* chore: add fallback for the live server

* fix: update provider document after patch request

* chore: make the health check call only on connection fail

* chore: update debounce interval

* refactor: remove useSwr call for healtch check

* fix: pages fallback init
This commit is contained in:
Aaryan Khandelwal 2024-09-23 15:35:06 +05:30 committed by GitHub
parent ae1a63f832
commit f9a8896486
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 165 additions and 74 deletions

View file

@ -34,5 +34,6 @@ export const getHocusPocusServer = async () => {
} }
}, },
extensions, extensions,
debounce: 10000
}); });
}; };

View file

@ -0,0 +1,16 @@
import * as Y from "yjs";
/**
* @description apply updates to a doc and return the updated doc in base64(binary) format
* @param {Uint8Array} document
* @param {Uint8Array} updates
* @returns {string} base64(binary) form of the updated doc
*/
export const applyUpdates = (document: Uint8Array, updates: Uint8Array): Uint8Array => {
const yDoc = new Y.Doc();
Y.applyUpdate(yDoc, document);
Y.applyUpdate(yDoc, updates);
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
return encodedDoc;
};

View file

@ -68,10 +68,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
editorProps, editorProps,
editorClassName, editorClassName,
enableHistory: false, enableHistory: false,
fileHandler,
handleEditorReady,
forwardedRef,
mentionHandler,
extensions: [ extensions: [
SideMenuExtension({ SideMenuExtension({
aiEnabled: !disabledExtensions?.includes("ai"), aiEnabled: !disabledExtensions?.includes("ai"),
@ -88,7 +84,12 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
userDetails: user, userDetails: user,
}), }),
], ],
fileHandler,
handleEditorReady,
forwardedRef,
mentionHandler,
placeholder, placeholder,
provider,
tabIndex, tabIndex,
}); });

View file

@ -1,8 +1,10 @@
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react"; import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import { DOMSerializer } from "@tiptap/pm/model"; import { DOMSerializer } from "@tiptap/pm/model";
import { Selection } from "@tiptap/pm/state"; import { Selection } from "@tiptap/pm/state";
import { EditorProps } from "@tiptap/pm/view"; import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useTiptapEditor, Editor } from "@tiptap/react"; import { useEditor as useTiptapEditor, Editor } from "@tiptap/react";
import * as Y from "yjs";
// components // components
import { getEditorMenuItems } from "@/components/menus"; import { getEditorMenuItems } from "@/components/menus";
// extensions // extensions
@ -32,6 +34,7 @@ export interface CustomEditorProps {
}; };
onChange?: (json: object, html: string) => void; onChange?: (json: object, html: string) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string); placeholder?: string | ((isFocused: boolean, value: string) => string);
provider?: HocuspocusProvider;
tabIndex?: number; tabIndex?: number;
// undefined when prop is not passed, null if intentionally passed to stop // undefined when prop is not passed, null if intentionally passed to stop
// swr syncing // swr syncing
@ -52,6 +55,7 @@ export const useEditor = (props: CustomEditorProps) => {
mentionHandler, mentionHandler,
onChange, onChange,
placeholder, placeholder,
provider,
tabIndex, tabIndex,
value, value,
} = props; } = props;
@ -186,9 +190,16 @@ export const useEditor = (props: CustomEditorProps) => {
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown(); const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
return markdownOutput; return markdownOutput;
}, },
getHTML: (): string => { getDocument: () => {
const htmlOutput = editorRef.current?.getHTML() ?? "<p></p>"; const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
return htmlOutput; const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
const documentJSON = editorRef.current?.getJSON() ?? null;
return {
binary: documentBinary,
html: documentHTML,
json: documentJSON,
};
}, },
scrollSummary: (marking: IMarking): void => { scrollSummary: (marking: IMarking): void => {
if (!editorRef.current) return; if (!editorRef.current) return;
@ -259,6 +270,11 @@ export const useEditor = (props: CustomEditorProps) => {
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0, words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
}; };
}, },
setProviderDocument: (value) => {
const document = provider?.document;
if (!document) return;
Y.applyUpdate(document, value);
},
}), }),
[editorRef, savedSelection] [editorRef, savedSelection]
); );

View file

@ -54,15 +54,16 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
const editor = useReadOnlyEditor({ const editor = useReadOnlyEditor({
editorProps, editorProps,
editorClassName, editorClassName,
forwardedRef,
handleEditorReady,
mentionHandler,
extensions: [ extensions: [
...(extensions ?? []), ...(extensions ?? []),
Collaboration.configure({ Collaboration.configure({
document: provider.document, document: provider.document,
}), }),
], ],
forwardedRef,
handleEditorReady,
mentionHandler,
provider,
}); });
return { editor, isIndexedDbSynced: true }; return { editor, isIndexedDbSynced: true };

View file

@ -1,6 +1,8 @@
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react"; import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import { EditorProps } from "@tiptap/pm/view"; import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import * as Y from "yjs";
// extensions // extensions
import { CoreReadOnlyEditorExtensions } from "@/extensions"; import { CoreReadOnlyEditorExtensions } from "@/extensions";
// helpers // helpers
@ -21,9 +23,11 @@ interface CustomReadOnlyEditorProps {
mentionHandler: { mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>; highlights: () => Promise<IMentionHighlight[]>;
}; };
provider?: HocuspocusProvider;
} }
export const useReadOnlyEditor = ({ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
const {
initialValue, initialValue,
editorClassName, editorClassName,
forwardedRef, forwardedRef,
@ -31,7 +35,9 @@ export const useReadOnlyEditor = ({
editorProps = {}, editorProps = {},
handleEditorReady, handleEditorReady,
mentionHandler, mentionHandler,
}: CustomReadOnlyEditorProps) => { provider,
} = props;
const editor = useCustomEditor({ const editor = useCustomEditor({
editable: false, editable: false,
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>", content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
@ -74,9 +80,16 @@ export const useReadOnlyEditor = ({
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown(); const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
return markdownOutput; return markdownOutput;
}, },
getHTML: (): string => { getDocument: () => {
const htmlOutput = editorRef.current?.getHTML() ?? "<p></p>"; const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
return htmlOutput; const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
const documentJSON = editorRef.current?.getJSON() ?? null;
return {
binary: documentBinary,
html: documentHTML,
json: documentJSON,
};
}, },
scrollSummary: (marking: IMarking): void => { scrollSummary: (marking: IMarking): void => {
if (!editorRef.current) return; if (!editorRef.current) return;

View file

@ -1,3 +1,4 @@
import { JSONContent } from "@tiptap/core";
// helpers // helpers
import { IMarking } from "@/helpers/scroll-to-node"; import { IMarking } from "@/helpers/scroll-to-node";
// types // types
@ -16,7 +17,11 @@ import {
// editor refs // editor refs
export type EditorReadOnlyRefApi = { export type EditorReadOnlyRefApi = {
getMarkDown: () => string; getMarkDown: () => string;
getHTML: () => string; getDocument: () => {
binary: Uint8Array | null;
html: string;
json: JSONContent | null;
};
clearEditor: (emitUpdate?: boolean) => void; clearEditor: (emitUpdate?: boolean) => void;
setEditorValue: (content: string) => void; setEditorValue: (content: string) => void;
scrollSummary: (marking: IMarking) => void; scrollSummary: (marking: IMarking) => void;
@ -38,6 +43,7 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
isEditorReadyToDiscard: () => boolean; isEditorReadyToDiscard: () => boolean;
getSelectedText: () => string | null; getSelectedText: () => string | null;
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void; insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
setProviderDocument: (value: Uint8Array) => void;
} }
// editor props // editor props

View file

@ -21,6 +21,7 @@ export { isCellSelection } from "@/extensions/table/table/utilities/is-cell-sele
// helpers // helpers
export * from "@/helpers/common"; export * from "@/helpers/common";
export * from "@/helpers/editor-commands"; export * from "@/helpers/editor-commands";
export * from "@/helpers/yjs";
export * from "@/extensions/table/table"; export * from "@/extensions/table/table";
// components // components

View file

@ -64,3 +64,9 @@ export type TPageVersion = {
updated_by: string; updated_by: string;
workspace: string; workspace: string;
} }
export type TDocumentPayload = {
description_binary: string;
description_html: string;
description: object;
}

View file

@ -205,7 +205,6 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
}, },
}} }}
realtimeConfig={realtimeConfig} realtimeConfig={realtimeConfig}
serverHandler={serverHandler}
user={{ user={{
id: currentUser?.id ?? "", id: currentUser?.id ?? "",
name: currentUser?.display_name ?? "", name: currentUser?.display_name ?? "",

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CircleAlert } from "lucide-react";
// editor // editor
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
// ui // ui
@ -19,13 +18,12 @@ import { IPage } from "@/store/pages/page";
type Props = { type Props = {
editorRef: React.RefObject<EditorRefApi>; editorRef: React.RefObject<EditorRefApi>;
handleDuplicatePage: () => void; handleDuplicatePage: () => void;
hasConnectionFailed: boolean;
page: IPage; page: IPage;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>; readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
}; };
export const PageExtraOptions: React.FC<Props> = observer((props) => { export const PageExtraOptions: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, hasConnectionFailed, page, readOnlyEditorRef } = props; const { editorRef, handleDuplicatePage, page, readOnlyEditorRef } = props;
// derived values // derived values
const { const {
archived_at, archived_at,
@ -79,17 +77,6 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
</div> </div>
</Tooltip> </Tooltip>
)} )}
{hasConnectionFailed && isOnline && (
<Tooltip
tooltipHeading="Connection failed"
tooltipContent="All changes made will be saved locally and will be synced when the connection is re-established."
>
<div className="flex-shrink-0 flex h-7 items-center gap-2 rounded-full bg-red-500/20 px-3 py-0.5 text-xs font-medium text-red-500">
<CircleAlert className="flex-shrink-0 size-3" />
<span>Server error</span>
</div>
</Tooltip>
)}
{canCurrentUserFavoritePage && ( {canCurrentUserFavoritePage && (
<FavoriteStar <FavoriteStar
selected={is_favorite} selected={is_favorite}

View file

@ -12,7 +12,6 @@ type Props = {
editorReady: boolean; editorReady: boolean;
editorRef: React.RefObject<EditorRefApi>; editorRef: React.RefObject<EditorRefApi>;
handleDuplicatePage: () => void; handleDuplicatePage: () => void;
hasConnectionFailed: boolean;
page: IPage; page: IPage;
readOnlyEditorReady: boolean; readOnlyEditorReady: boolean;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>; readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
@ -25,7 +24,6 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
editorReady, editorReady,
editorRef, editorRef,
handleDuplicatePage, handleDuplicatePage,
hasConnectionFailed,
page, page,
readOnlyEditorReady, readOnlyEditorReady,
readOnlyEditorRef, readOnlyEditorRef,
@ -53,7 +51,6 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
<PageExtraOptions <PageExtraOptions
editorRef={editorRef} editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage} handleDuplicatePage={handleDuplicatePage}
hasConnectionFailed={hasConnectionFailed}
page={page} page={page}
readOnlyEditorRef={readOnlyEditorRef} readOnlyEditorRef={readOnlyEditorRef}
/> />

View file

@ -14,7 +14,6 @@ type Props = {
editorReady: boolean; editorReady: boolean;
editorRef: React.RefObject<EditorRefApi>; editorRef: React.RefObject<EditorRefApi>;
handleDuplicatePage: () => void; handleDuplicatePage: () => void;
hasConnectionFailed: boolean;
page: IPage; page: IPage;
readOnlyEditorReady: boolean; readOnlyEditorReady: boolean;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>; readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
@ -27,7 +26,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
editorReady, editorReady,
editorRef, editorRef,
handleDuplicatePage, handleDuplicatePage,
hasConnectionFailed,
page, page,
readOnlyEditorReady, readOnlyEditorReady,
readOnlyEditorRef, readOnlyEditorRef,
@ -67,7 +65,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
<PageExtraOptions <PageExtraOptions
editorRef={editorRef} editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage} handleDuplicatePage={handleDuplicatePage}
hasConnectionFailed={hasConnectionFailed}
page={page} page={page}
readOnlyEditorRef={readOnlyEditorRef} readOnlyEditorRef={readOnlyEditorRef}
/> />
@ -79,7 +76,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
editorReady={editorReady} editorReady={editorReady}
readOnlyEditorReady={readOnlyEditorReady} readOnlyEditorReady={readOnlyEditorReady}
handleDuplicatePage={handleDuplicatePage} handleDuplicatePage={handleDuplicatePage}
hasConnectionFailed={hasConnectionFailed}
page={page} page={page}
sidePeekVisible={sidePeekVisible} sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible} setSidePeekVisible={setSidePeekVisible}

View file

@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
// editor // editor
import { EditorRefApi } from "@plane/editor"; import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
// types // types
import { TPage } from "@plane/types"; import { TPage } from "@plane/types";
// ui // ui
@ -12,9 +12,11 @@ import { PageEditorHeaderRoot, PageEditorBody, PageVersionsOverlay, PagesVersion
// hooks // hooks
import { useProjectPages } from "@/hooks/store"; import { useProjectPages } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { usePageFallback } from "@/hooks/use-page-fallback";
import { useQueryParams } from "@/hooks/use-query-params"; import { useQueryParams } from "@/hooks/use-query-params";
// services // services
import { ProjectPageVersionService } from "@/services/page"; import { ProjectPageService, ProjectPageVersionService } from "@/services/page";
const projectPageService = new ProjectPageService();
const projectPageVersionService = new ProjectPageVersionService(); const projectPageVersionService = new ProjectPageVersionService();
// store // store
import { IPage } from "@/store/pages/page"; import { IPage } from "@/store/pages/page";
@ -29,8 +31,8 @@ export const PageRoot = observer((props: TPageRootProps) => {
const { projectId, workspaceSlug, page } = props; const { projectId, workspaceSlug, page } = props;
// states // states
const [editorReady, setEditorReady] = useState(false); const [editorReady, setEditorReady] = useState(false);
const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false);
const [hasConnectionFailed, setHasConnectionFailed] = useState(false); const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false);
const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768); const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768);
const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false);
// refs // refs
@ -43,8 +45,17 @@ export const PageRoot = observer((props: TPageRootProps) => {
// store hooks // store hooks
const { createPage } = useProjectPages(); const { createPage } = useProjectPages();
// derived values // derived values
const { access, description_html, name, isContentEditable } = page; const { access, description_html, name, isContentEditable, updateDescription } = page;
// page fallback
usePageFallback({
editorRef,
fetchPageDescription: async () => {
if (!page.id) return;
return await projectPageService.fetchDescriptionBinary(workspaceSlug, projectId, page.id);
},
hasConnectionFailed,
updatePageDescription: async (data) => await updateDescription(data),
});
// update query params // update query params
const { updateQueryParams } = useQueryParams(); const { updateQueryParams } = useQueryParams();
@ -53,7 +64,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
const handleDuplicatePage = async () => { const handleDuplicatePage = async () => {
const formData: Partial<TPage> = { const formData: Partial<TPage> = {
name: "Copy of " + name, name: "Copy of " + name,
description_html: editorRef.current?.getHTML() ?? description_html ?? "<p></p>", description_html: editorRef.current?.getDocument().html ?? description_html ?? "<p></p>",
access, access,
}; };
@ -89,8 +100,8 @@ export const PageRoot = observer((props: TPageRootProps) => {
editorRef.current?.setEditorValue(descriptionHTML); editorRef.current?.setEditorValue(descriptionHTML);
}; };
const currentVersionDescription = isContentEditable const currentVersionDescription = isContentEditable
? editorRef.current?.getHTML() ? editorRef.current?.getDocument().html
: readOnlyEditorRef.current?.getHTML(); : readOnlyEditorRef.current?.getDocument().html;
return ( return (
<> <>
@ -125,7 +136,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
editorReady={editorReady} editorReady={editorReady}
editorRef={editorRef} editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage} handleDuplicatePage={handleDuplicatePage}
hasConnectionFailed={hasConnectionFailed}
page={page} page={page}
readOnlyEditorReady={readOnlyEditorReady} readOnlyEditorReady={readOnlyEditorReady}
readOnlyEditorRef={readOnlyEditorRef} readOnlyEditorRef={readOnlyEditorRef}

View file

@ -1,9 +1,9 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { debounce } from "lodash"; import { debounce } from "lodash";
const AUTO_SAVE_TIME = 10000; const AUTO_SAVE_TIME = 30000;
const useAutoSave = (handleSaveDescription: (forceSync?: boolean, yjsAsUpdate?: Uint8Array) => void) => { const useAutoSave = (handleSaveDescription: () => void) => {
const intervalIdRef = useRef<any>(null); const intervalIdRef = useRef<any>(null);
const handleSaveDescriptionRef = useRef(handleSaveDescription); const handleSaveDescriptionRef = useRef(handleSaveDescription);
@ -16,7 +16,7 @@ const useAutoSave = (handleSaveDescription: (forceSync?: boolean, yjsAsUpdate?:
useEffect(() => { useEffect(() => {
intervalIdRef.current = setInterval(() => { intervalIdRef.current = setInterval(() => {
try { try {
handleSaveDescriptionRef.current(true); handleSaveDescriptionRef.current();
} catch (error) { } catch (error) {
console.error("Autosave before manual save failed:", error); console.error("Autosave before manual save failed:", error);
} }
@ -43,7 +43,7 @@ const useAutoSave = (handleSaveDescription: (forceSync?: boolean, yjsAsUpdate?:
clearInterval(intervalIdRef.current); clearInterval(intervalIdRef.current);
intervalIdRef.current = setInterval(() => { intervalIdRef.current = setInterval(() => {
try { try {
handleSaveDescriptionRef.current(true); handleSaveDescriptionRef.current();
} catch (error) { } catch (error) {
console.error("Autosave after manual save failed:", error); console.error("Autosave after manual save failed:", error);
} }

View file

@ -0,0 +1,48 @@
import { useCallback, useEffect } from "react";
// plane editor
import { EditorRefApi } from "@plane/editor";
// plane types
import { TDocumentPayload } from "@plane/types";
// hooks
import useAutoSave from "@/hooks/use-auto-save";
type TArgs = {
editorRef: React.RefObject<EditorRefApi>;
fetchPageDescription: () => Promise<any>;
hasConnectionFailed: boolean;
updatePageDescription: (data: TDocumentPayload) => Promise<void>;
};
export const usePageFallback = (args: TArgs) => {
const { editorRef, fetchPageDescription, hasConnectionFailed, updatePageDescription } = args;
const handleUpdateDescription = useCallback(async () => {
if (!hasConnectionFailed) return;
const editor = editorRef.current;
if (!editor) return;
const latestEncodedDescription = await fetchPageDescription();
const latestDecodedDescription = latestEncodedDescription
? new Uint8Array(latestEncodedDescription)
: new Uint8Array();
editor.setProviderDocument(latestDecodedDescription);
const { binary, html, json } = editor.getDocument();
if (!binary || !json) return;
const encodedBinary = Buffer.from(binary).toString("base64");
await updatePageDescription({
description_binary: encodedBinary,
description_html: html,
description: json,
});
}, [hasConnectionFailed]);
useEffect(() => {
if (hasConnectionFailed) {
handleUpdateDescription();
}
}, [hasConnectionFailed]);
useAutoSave(handleUpdateDescription);
};

View file

@ -1,5 +1,5 @@
// types // types
import { TPage } from "@plane/types"; import { TDocumentPayload, TPage } from "@plane/types";
// helpers // helpers
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
// services // services
@ -128,7 +128,7 @@ export class ProjectPageService extends APIService {
}); });
} }
async fetchDescriptionYJS(workspaceSlug: string, projectId: string, pageId: string): Promise<any> { async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
headers: { headers: {
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
@ -145,10 +145,7 @@ export class ProjectPageService extends APIService {
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
pageId: string, pageId: string,
data: { data: TDocumentPayload
description_binary: string;
description_html: string;
}
): Promise<any> { ): Promise<any> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, data) return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, data)
.then((response) => response?.data) .then((response) => response?.data)

View file

@ -1,7 +1,7 @@
import set from "lodash/set"; import set from "lodash/set";
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx"; import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
// types // types
import { TLogoProps, TPage } from "@plane/types"; import { TDocumentPayload, TLogoProps, TPage } from "@plane/types";
// constants // constants
import { EPageAccess } from "@/constants/page"; import { EPageAccess } from "@/constants/page";
import { EUserPermissions } from "@/plane-web/constants/user-permissions"; import { EUserPermissions } from "@/plane-web/constants/user-permissions";
@ -33,7 +33,7 @@ export interface IPage extends TPage {
// actions // actions
update: (pageData: Partial<TPage>) => Promise<TPage | undefined>; update: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
updateTitle: (title: string) => void; updateTitle: (title: string) => void;
updateDescription: (binaryString: string, descriptionHTML: string) => Promise<void>; updateDescription: (document: TDocumentPayload) => Promise<void>;
makePublic: () => Promise<void>; makePublic: () => Promise<void>;
makePrivate: () => Promise<void>; makePrivate: () => Promise<void>;
lock: () => Promise<void>; lock: () => Promise<void>;
@ -367,23 +367,19 @@ export class Page implements IPage {
/** /**
* @description update the page description * @description update the page description
* @param {string} binaryString * @param {TDocumentPayload} document
* @param {string} descriptionHTML
*/ */
updateDescription = async (binaryString: string, descriptionHTML: string) => { updateDescription = async (document: TDocumentPayload) => {
const { workspaceSlug, projectId } = this.store.router; const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId || !this.id) return undefined; if (!workspaceSlug || !projectId || !this.id) return undefined;
const currentDescription = this.description_html; const currentDescription = this.description_html;
runInAction(() => { runInAction(() => {
this.description_html = descriptionHTML; this.description_html = document.description_html;
}); });
try { try {
await this.pageService.updateDescriptionYJS(workspaceSlug, projectId, this.id, { await this.pageService.updateDescriptionYJS(workspaceSlug, projectId, this.id, document);
description_binary: binaryString,
description_html: descriptionHTML,
});
} catch (error) { } catch (error) {
runInAction(() => { runInAction(() => {
this.description_html = currentDescription; this.description_html = currentDescription;