Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-1116] fix: page outline not reflecting changes in realtime #5567

Merged
merged 8 commits into from
Sep 23, 2024
2 changes: 1 addition & 1 deletion packages/editor/src/core/components/menus/menu-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export const ImageItem = (editor: Editor) =>
({
key: "image",
name: "Image",
isActive: () => editor?.isActive("image"),
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
command: (savedSelection: Selection | null) =>
editor?.commands.setImageUpload({ event: "insert", pos: savedSelection?.from }),
icon: ImageIcon,
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/core/extensions/drop.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";

export const DropHandlerExtension = () =>
Extension.create({
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/core/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
CustomQuoteExtension,
CustomTypographyExtension,
DropHandlerExtension,
HeadingListExtension,
ImageExtension,
ListKeymap,
Table,
Expand Down Expand Up @@ -166,4 +167,5 @@ export const CoreEditorExtensions = ({
includeChildren: true,
}),
CharacterCount,
HeadingListExtension,
];
57 changes: 57 additions & 0 deletions packages/editor/src/core/extensions/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";

export interface IMarking {
type: "heading";
level: number;
text: string;
sequence: number;
}

export const HeadingListExtension = Extension.create({
name: "headingList",

addStorage() {
return {
headings: [] as IMarking[],
};
},

addProseMirrorPlugins() {
const plugin = new Plugin({
key: new PluginKey("heading-list"),
appendTransaction: (_, __, newState) => {
const headings: IMarking[] = [];
let h1Sequence = 0;
let h2Sequence = 0;
let h3Sequence = 0;

newState.doc.descendants((node) => {
if (node.type.name === "heading") {
const level = node.attrs.level;
const text = node.textContent;

headings.push({
type: "heading",
level: level,
text: text,
sequence: level === 1 ? ++h1Sequence : level === 2 ? ++h2Sequence : ++h3Sequence,
});
}
});

this.storage.headings = headings;

this.editor.emit("update", { editor: this.editor, transaction: newState.tr });

return null;
},
});

return [plugin];
},

getHeadings() {
return this.storage.headings;
},
});
1 change: 1 addition & 0 deletions packages/editor/src/core/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from "./quote";
export * from "./read-only-extensions";
export * from "./side-menu";
export * from "./slash-commands";
export * from "./headers";
2 changes: 2 additions & 0 deletions packages/editor/src/core/extensions/read-only-extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
TableRow,
Table,
CustomMention,
HeadingListExtension,
CustomReadOnlyImageExtension,
} from "@/extensions";
// helpers
Expand Down Expand Up @@ -108,4 +109,5 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
readonly: true,
}),
CharacterCount,
HeadingListExtension,
];
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model";
import { Fragment, Node as ProsemirrorNode, NodeType } from "@tiptap/pm/model";

export function createCell(
cellType: NodeType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NodeType, Schema } from "prosemirror-model";
import { NodeType, Schema } from "@tiptap/pm/model";

export function getTableNodeTypes(schema: Schema): { [key: string]: NodeType } {
if (schema.cached.tableNodeTypes) {
Expand Down
16 changes: 16 additions & 0 deletions packages/editor/src/core/hooks/use-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,27 @@ export const useEditor = (props: CustomEditorProps) => {
const item = getEditorMenuItem(itemName);
return item ? item.isActive() : false;
},
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension
editorRef.current?.on("update", () => {
callback(editorRef.current?.storage.headingList.headings);
});
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editorRef.current?.off("update");
};
},
getHeadings: () => {
return editorRef?.current?.storage.headingList.headings;
},
onStateChange: (callback: () => void) => {
// Subscribe to editor state changes
editorRef.current?.on("transaction", () => {
callback();
});

// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
Expand Down
15 changes: 15 additions & 0 deletions packages/editor/src/core/hooks/use-read-only-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ export const useReadOnlyEditor = ({
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
};
},
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension
editorRef.current?.on("update", () => {
callback(editorRef.current?.storage.headingList.headings);
});
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editorRef.current?.off("update");
};
},
getHeadings: () => {
return editorRef?.current?.storage.headingList.headings;
},
}));

if (!editor) {
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/core/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export type EditorReadOnlyRefApi = {
paragraphs: number;
words: number;
};
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
};

export interface EditorRefApi extends EditorReadOnlyRefApi {
Expand Down
20 changes: 5 additions & 15 deletions web/core/components/pages/editor/editor-body.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from "react";
import { useCallback, useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// document-editor
Expand Down Expand Up @@ -38,14 +38,13 @@ const fileService = new FileService();

type Props = {
editorRef: React.RefObject<EditorRefApi>;
editorReady: boolean;
handleConnectionStatus: (status: boolean) => void;
handleEditorReady: (value: boolean) => void;
handleReadOnlyEditorReady: (value: boolean) => void;
markings: IMarking[];
page: IPage;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
sidePeekVisible: boolean;
updateMarkings: (description_html: string) => void;
};

export const PageEditorBody: React.FC<Props> = observer((props) => {
Expand All @@ -54,11 +53,9 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
handleConnectionStatus,
handleEditorReady,
handleReadOnlyEditorReady,
markings,
page,
readOnlyEditorRef,
sidePeekVisible,
updateMarkings,
} = props;
// router
const { workspaceSlug, projectId } = useParams();
Expand All @@ -70,10 +67,9 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
project: { getProjectMemberIds },
} = useMember();
// derived values
const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : "";
const workspaceId = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "" : "";
const pageId = page?.id;
const pageTitle = page?.name ?? "";
const pageDescription = page?.description_html;
const { isContentEditable, updateTitle, setIsSubmitting } = page;
const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : [];
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
Expand Down Expand Up @@ -104,6 +100,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
const handleServerConnect = useCallback(() => {
handleConnectionStatus(false);
}, []);

const handleServerError = useCallback(() => {
handleConnectionStatus(true);
}, []);
Expand All @@ -116,10 +113,6 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
[]
);

useEffect(() => {
updateMarkings(pageDescription ?? "<p></p>");
}, [pageDescription, updateMarkings]);

const realtimeConfig: TRealtimeConfig = useMemo(
() => ({
url: `${LIVE_URL}/collaboration`,
Expand All @@ -144,10 +137,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
})}
>
{!isFullWidth && (
<PageContentBrowser
editorRef={(isContentEditable ? editorRef : readOnlyEditorRef)?.current}
markings={markings}
/>
<PageContentBrowser editorRef={(isContentEditable ? editorRef : readOnlyEditorRef)?.current} />
)}
</Row>
<div
Expand Down
3 changes: 0 additions & 3 deletions web/core/components/pages/editor/header/mobile-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ type Props = {
editorRef: React.RefObject<EditorRefApi>;
handleDuplicatePage: () => void;
hasConnectionFailed: boolean;
markings: IMarking[];
page: IPage;
readOnlyEditorReady: boolean;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
Expand All @@ -27,7 +26,6 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
editorRef,
handleDuplicatePage,
hasConnectionFailed,
markings,
page,
readOnlyEditorReady,
readOnlyEditorRef,
Expand All @@ -48,7 +46,6 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
<PageSummaryPopover
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
isFullWidth={isFullWidth}
markings={markings}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible}
/>
Expand Down
34 changes: 16 additions & 18 deletions web/core/components/pages/editor/header/root.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor";
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
// components
import { Header, EHeaderVariant } from "@plane/ui";
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
Expand All @@ -15,7 +15,6 @@ type Props = {
editorRef: React.RefObject<EditorRefApi>;
handleDuplicatePage: () => void;
hasConnectionFailed: boolean;
markings: IMarking[];
page: IPage;
readOnlyEditorReady: boolean;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
Expand All @@ -29,7 +28,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
editorRef,
handleDuplicatePage,
hasConnectionFailed,
markings,
page,
readOnlyEditorReady,
readOnlyEditorRef,
Expand All @@ -47,20 +45,21 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
<>
<Header variant={EHeaderVariant.SECONDARY} showOnMobile={false}>
<Header.LeftItem className="gap-0 w-full">
<div
className={cn("flex-shrink-0 my-auto", {
"w-40 lg:w-56": !isFullWidth,
"w-[5%]": isFullWidth,
})}
>
<PageSummaryPopover
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
isFullWidth={isFullWidth}
markings={markings}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible}
/>
</div>
{(editorReady || readOnlyEditorReady) && (
<div
className={cn("flex-shrink-0 my-auto", {
"w-40 lg:w-56": !isFullWidth,
"w-[5%]": isFullWidth,
})}
>
<PageSummaryPopover
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
isFullWidth={isFullWidth}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible}
/>
</div>
)}
{(editorReady || readOnlyEditorReady) && isContentEditable && editorRef.current && (
<PageToolbar editorRef={editorRef?.current} />
)}
Expand All @@ -79,7 +78,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
readOnlyEditorRef={readOnlyEditorRef}
editorReady={editorReady}
readOnlyEditorReady={readOnlyEditorReady}
markings={markings}
handleDuplicatePage={handleDuplicatePage}
hasConnectionFailed={hasConnectionFailed}
page={page}
Expand Down
9 changes: 3 additions & 6 deletions web/core/components/pages/editor/page-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
// editor
import { EditorReadOnlyRefApi, EditorRefApi, useEditorMarkings } from "@plane/editor";
import { EditorRefApi } from "@plane/editor";
// types
import { TPage } from "@plane/types";
// ui
Expand Down Expand Up @@ -44,8 +44,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
const { createPage } = useProjectPages();
// derived values
const { access, description_html, name, isContentEditable } = page;
// editor markings hook
const { markings, updateMarkings } = useEditorMarkings();

// update query params
const { updateQueryParams } = useQueryParams();

Expand Down Expand Up @@ -127,23 +126,21 @@ export const PageRoot = observer((props: TPageRootProps) => {
editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage}
hasConnectionFailed={hasConnectionFailed}
markings={markings}
page={page}
readOnlyEditorReady={readOnlyEditorReady}
readOnlyEditorRef={readOnlyEditorRef}
setSidePeekVisible={(state) => setSidePeekVisible(state)}
sidePeekVisible={sidePeekVisible}
/>
<PageEditorBody
editorReady={editorReady}
editorRef={editorRef}
handleConnectionStatus={(status) => setHasConnectionFailed(status)}
handleEditorReady={(val) => setEditorReady(val)}
handleReadOnlyEditorReady={() => setReadOnlyEditorReady(true)}
markings={markings}
page={page}
readOnlyEditorRef={readOnlyEditorRef}
sidePeekVisible={sidePeekVisible}
updateMarkings={updateMarkings}
/>
</>
);
Expand Down
Loading
Loading