[WEB-1727] refactor: pages editor sync logic solidified (#4926)

* feat: pages editor sync logic solidified

* chore: added validation for archive and lock in a page

* feat: pages editor sync logic solidified

* fix: updated the auto save hook to run every 10s instead of 10s after the user stops typing!!

* chore: custom status code for pages

* fix: forceSync in case of auto save

* fix: modifying a locked and archived page shows a toast for now!

* fix: build errors and better error messages

* chore: page root moved

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
M. Palanikannan 2024-06-25 18:58:57 +05:30 committed by GitHub
parent c919435598
commit 99184371f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 444 additions and 201 deletions

View file

@ -17,7 +17,6 @@ import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/compon
import { cn } from "@/helpers/common.helper";
// hooks
import { useMember, useMention, useUser, useWorkspace } from "@/hooks/store";
import { usePageDescription } from "@/hooks/use-page-description";
import { usePageFilters } from "@/hooks/use-page-filters";
// plane web components
import { IssueEmbedCard } from "@/plane-web/components/pages";
@ -37,6 +36,9 @@ type Props = {
handleEditorReady: (value: boolean) => void;
handleReadOnlyEditorReady: (value: boolean) => void;
updateMarkings: (description_html: string) => void;
handleDescriptionChange: (update: Uint8Array, source?: string | undefined) => void;
isDescriptionReady: boolean;
pageDescriptionYJS: Uint8Array | undefined;
};
export const PageEditorBody: React.FC<Props> = observer((props) => {
@ -49,6 +51,9 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
page,
sidePeekVisible,
updateMarkings,
handleDescriptionChange,
isDescriptionReady,
pageDescriptionYJS,
} = props;
// router
const { workspaceSlug, projectId } = useParams();
@ -67,13 +72,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
const { isContentEditable, updateTitle, setIsSubmitting } = page;
const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : [];
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
// project-description
const { handleDescriptionChange, isDescriptionReady, pageDescriptionYJS } = usePageDescription({
editorRef,
page,
projectId,
workspaceSlug,
});
// use-mention
const { mentionHighlights, mentionSuggestions } = useMention({
workspaceSlug: workspaceSlug?.toString() ?? "",

View file

@ -23,10 +23,11 @@ type Props = {
handleDuplicatePage: () => void;
page: IPage;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
handleSaveDescription: (forceSync?: boolean, initSyncVectorAsUpdate?: Uint8Array | undefined) => Promise<void>;
};
export const PageExtraOptions: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, page, readOnlyEditorRef } = props;
const { editorRef, handleDuplicatePage, page, readOnlyEditorRef, handleSaveDescription } = props;
// states
const [gptModalOpen, setGptModal] = useState(false);
// store hooks
@ -76,6 +77,7 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
handleDuplicatePage={handleDuplicatePage}
page={page}
handleSaveDescription={handleSaveDescription}
/>
</div>
);

View file

@ -17,6 +17,7 @@ type Props = {
setSidePeekVisible: (sidePeekState: boolean) => void;
editorReady: boolean;
readOnlyEditorReady: boolean;
handleSaveDescription: (forceSync?: boolean, initSyncVectorAsUpdate?: Uint8Array | undefined) => Promise<void>;
};
export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
@ -30,6 +31,7 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
page,
sidePeekVisible,
setSidePeekVisible,
handleSaveDescription,
} = props;
// derived values
const { isContentEditable } = page;
@ -52,6 +54,7 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
</div>
<PageExtraOptions
editorRef={editorRef}
handleSaveDescription={handleSaveDescription}
handleDuplicatePage={handleDuplicatePage}
page={page}
readOnlyEditorRef={readOnlyEditorRef}

View file

@ -18,10 +18,11 @@ type Props = {
editorRef: EditorRefApi | EditorReadOnlyRefApi | null;
handleDuplicatePage: () => void;
page: IPage;
handleSaveDescription: (forceSync?: boolean, initSyncVectorAsUpdate?: Uint8Array | undefined) => Promise<void>;
};
export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, page } = props;
const { editorRef, handleDuplicatePage, page, handleSaveDescription } = props;
// store values
const {
archived_at,
@ -75,6 +76,11 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
})
);
const saveDescriptionYJSAndPerformAction = (action: () => void) => async () => {
await handleSaveDescription();
action();
};
// menu items list
const MENU_ITEMS: {
key: string;
@ -116,21 +122,21 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
},
{
key: "make-a-copy",
action: handleDuplicatePage,
action: saveDescriptionYJSAndPerformAction(handleDuplicatePage),
label: "Make a copy",
icon: Copy,
shouldRender: canCurrentUserDuplicatePage,
},
{
key: "lock-unlock-page",
action: is_locked ? handleUnlockPage : handleLockPage,
action: is_locked ? handleUnlockPage : saveDescriptionYJSAndPerformAction(handleLockPage),
label: is_locked ? "Unlock page" : "Lock page",
icon: is_locked ? LockOpen : Lock,
shouldRender: canCurrentUserLockPage,
},
{
key: "archive-restore-page",
action: archived_at ? handleRestorePage : handleArchivePage,
action: archived_at ? handleRestorePage : saveDescriptionYJSAndPerformAction(handleArchivePage),
label: archived_at ? "Restore page" : "Archive page",
icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
shouldRender: canCurrentUserArchivePage,

View file

@ -19,6 +19,7 @@ type Props = {
setSidePeekVisible: (sidePeekState: boolean) => void;
editorReady: boolean;
readOnlyEditorReady: boolean;
handleSaveDescription: (forceSync?: boolean, initSyncVectorAsUpdate?: Uint8Array | undefined) => Promise<void>;
};
export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
@ -32,6 +33,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
page,
sidePeekVisible,
setSidePeekVisible,
handleSaveDescription,
} = props;
// derived values
const { isContentEditable } = page;
@ -63,12 +65,14 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
<PageExtraOptions
editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage}
handleSaveDescription={handleSaveDescription}
page={page}
readOnlyEditorRef={readOnlyEditorRef}
/>
</div>
<div className="md:hidden">
<PageEditorMobileHeaderRoot
handleSaveDescription={handleSaveDescription}
editorRef={editorRef}
readOnlyEditorRef={readOnlyEditorRef}
editorReady={editorReady}

View file

@ -0,0 +1,97 @@
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { EditorRefApi, useEditorMarkings } from "@plane/editor";
import { TPage } from "@plane/types";
import { setToast, TOAST_TYPE } from "@plane/ui";
import { PageEditorHeaderRoot, PageEditorBody } from "@/components/pages";
import { useProjectPages } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePageDescription } from "@/hooks/use-page-description";
import { IPage } from "@/store/pages/page";
type TPageRootProps = {
page: IPage;
projectId: string;
workspaceSlug: string;
};
export const PageRoot = observer((props: TPageRootProps) => {
// router
const router = useAppRouter();
const { projectId, workspaceSlug, page } = props;
const { createPage } = useProjectPages();
const { access, description_html, name } = page;
// states
const [editorReady, setEditorReady] = useState(false);
const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false);
// refs
const editorRef = useRef<EditorRefApi>(null);
const readOnlyEditorRef = useRef<EditorRefApi>(null);
// editor markings hook
const { markings, updateMarkings } = useEditorMarkings();
const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768 ? true : false);
// project-description
const { handleDescriptionChange, isDescriptionReady, pageDescriptionYJS, handleSaveDescription } = usePageDescription(
{
editorRef,
page,
projectId,
workspaceSlug,
}
);
const handleCreatePage = async (payload: Partial<TPage>) => await createPage(payload);
const handleDuplicatePage = async () => {
const formData: Partial<TPage> = {
name: "Copy of " + name,
description_html: editorRef.current?.getHTML() ?? description_html ?? "<p></p>",
access,
};
await handleCreatePage(formData)
.then((res) => router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res?.id}`))
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be duplicated. Please try again later.",
})
);
};
return (
<>
<PageEditorHeaderRoot
editorRef={editorRef}
readOnlyEditorRef={readOnlyEditorRef}
editorReady={editorReady}
readOnlyEditorReady={readOnlyEditorReady}
handleDuplicatePage={handleDuplicatePage}
handleSaveDescription={handleSaveDescription}
markings={markings}
page={page}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={(state) => setSidePeekVisible(state)}
/>
<PageEditorBody
editorRef={editorRef}
handleEditorReady={(val) => setEditorReady(val)}
readOnlyEditorRef={readOnlyEditorRef}
handleReadOnlyEditorReady={() => setReadOnlyEditorReady(true)}
markings={markings}
page={page}
sidePeekVisible={sidePeekVisible}
updateMarkings={updateMarkings}
handleDescriptionChange={handleDescriptionChange}
isDescriptionReady={isDescriptionReady}
pageDescriptionYJS={pageDescriptionYJS}
/>
</>
);
});