[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:
parent
ae1a63f832
commit
f9a8896486
18 changed files with 165 additions and 74 deletions
|
|
@ -34,5 +34,6 @@ export const getHocusPocusServer = async () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extensions,
|
extensions,
|
||||||
|
debounce: 10000
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
16
packages/editor/src/core/helpers/yjs.ts
Normal file
16
packages/editor/src/core/helpers/yjs.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
6
packages/types/src/pages.d.ts
vendored
6
packages/types/src/pages.d.ts
vendored
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 ?? "",
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
web/core/hooks/use-page-fallback.ts
Normal file
48
web/core/hooks/use-page-fallback.ts
Normal 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);
|
||||||
|
};
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue