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

fix: add lock unlock archive restore realtime sync #5629

Draft
wants to merge 21 commits into
base: preview
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
448a615
fix: add lock unlock archive restore realtime sync
Palanikannan1437 Sep 17, 2024
cd29430
fix: show only after editor loads
Palanikannan1437 Sep 17, 2024
8ce39f5
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Sep 23, 2024
c94fff9
fix: added strong types
Palanikannan1437 Sep 23, 2024
2c2dd62
fix: live events fixed
Palanikannan1437 Sep 24, 2024
c322122
fix: remove unused vars and logs
Palanikannan1437 Sep 24, 2024
e3d3ab5
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 1, 2024
83933ec
fix: converted objects to enum
Palanikannan1437 Oct 3, 2024
6f13c19
fix: error handling and removing the events in read only mode
Palanikannan1437 Oct 3, 2024
5a835e4
fix: added check to only update if the image aspect ratio is not pres…
Palanikannan1437 Oct 3, 2024
1732587
fix: imports
Palanikannan1437 Oct 3, 2024
20861a6
fix: props order
Palanikannan1437 Oct 3, 2024
ed751e3
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 3, 2024
2384f0b
revert: no need of these changes anymore
Palanikannan1437 Oct 3, 2024
bbe7e62
fix: updated type names
Palanikannan1437 Oct 3, 2024
b0bf242
fix: order of things
Palanikannan1437 Oct 3, 2024
b8c76eb
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 8, 2024
b42f552
fix: fixed types and renamed variables
Palanikannan1437 Oct 8, 2024
702236e
fix: better typing for the real time updates
Palanikannan1437 Oct 8, 2024
ee9e7f5
fix: trying multiplexing our socket connection
Palanikannan1437 Oct 8, 2024
b45761e
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion live/src/core/hocuspocus-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { v4 as uuidv4 } from "uuid";
import { handleAuthentication } from "@/core/lib/authentication.js";
// extensions
import { getExtensions } from "@/core/extensions/index.js";
import {
DocumentEventsServer,
documentEventResponses,
} from "@plane/editor/lib";

export const getHocusPocusServer = async () => {
const extensions = await getExtensions();
Expand Down Expand Up @@ -37,7 +41,13 @@ export const getHocusPocusServer = async () => {
throw Error("Authentication unsuccessful!");
}
},
async onStateless({ payload, document }) {
const response = documentEventResponses[payload as DocumentEventsServer];
if (response) {
document.broadcastStateless(response);
}
},
extensions,
debounce: 10000
debounce: 10000,
});
};
9 changes: 9 additions & 0 deletions packages/editor/src/core/helpers/document-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const documentEventResponses = {
Palanikannan1437 marked this conversation as resolved.
Show resolved Hide resolved
lock: "locked",
unlock: "unlocked",
archive: "archived",
unarchive: "unarchived",
} as const;

export type DocumentEventsServer = keyof typeof documentEventResponses;
export type DocumentEventsClient = (typeof documentEventResponses)[DocumentEventsServer];
3 changes: 3 additions & 0 deletions packages/editor/src/core/hooks/use-collaborative-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
onClose: (data) => {
if (data.event.code === 1006) serverHandler?.onServerError?.();
},
onSynced: () => {
serverHandler?.onSynced?.();
},
}),
[id, realtimeConfig, serverHandler, user.id]
);
Expand Down
9 changes: 6 additions & 3 deletions packages/editor/src/core/hooks/use-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
import { CoreEditorProps } from "@/props";
// types
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types";
import { DocumentEventsServer } from "src/lib";
Palanikannan1437 marked this conversation as resolved.
Show resolved Hide resolved

export interface CustomEditorProps {
editorClassName: string;
Expand All @@ -34,11 +35,11 @@ export interface CustomEditorProps {
};
onChange?: (json: object, html: string) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
provider?: HocuspocusProvider;
Palanikannan1437 marked this conversation as resolved.
Show resolved Hide resolved
tabIndex?: number;
// undefined when prop is not passed, null if intentionally passed to stop
// swr syncing
value?: string | null | undefined;
provider?: HocuspocusProvider;
}

export const useEditor = (props: CustomEditorProps) => {
Expand All @@ -55,9 +56,9 @@ export const useEditor = (props: CustomEditorProps) => {
mentionHandler,
onChange,
placeholder,
provider,
tabIndex,
value,
provider,
} = props;
// states
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
Expand Down Expand Up @@ -230,7 +231,7 @@ export const useEditor = (props: CustomEditorProps) => {
if (empty) return null;

const nodesArray: string[] = [];
state.doc.nodesBetween(from, to, (node, pos, parent) => {
state.doc.nodesBetween(from, to, (node, _pos, parent) => {
if (parent === state.doc && editorRef.current) {
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
const dom = serializer.serializeNode(node);
Expand Down Expand Up @@ -271,6 +272,8 @@ export const useEditor = (props: CustomEditorProps) => {
if (!document) return;
Y.applyUpdate(document, value);
},
emitRealTimeUpdate: (message: DocumentEventsServer) => provider?.sendStateless(message),
listenToRealTimeUpdate: () => provider,
}),
[editorRef, savedSelection]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
onClose: (data) => {
if (data.event.code === 1006) serverHandler?.onServerError?.();
},
onSynced: () => {
serverHandler?.onSynced?.();
},
}),
[id, realtimeConfig, user.id]
[id, realtimeConfig, serverHandler, user.id]
);

// destroy and disconnect connection on unmount
useEffect(
() => () => {
Expand Down
8 changes: 8 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 @@ -112,6 +112,14 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
editorRef.current?.off("update");
};
},
emitRealTimeUpdate: (message: string) => {
if (provider) {
provider.sendStateless(message);
}
},
listenToRealTimeUpdate: () => {
return provider;
},
getHeadings: () => editorRef?.current?.storage.headingList.headings,
}));

Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/core/types/collaboration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
export type TServerHandler = {
onConnect?: () => void;
onServerError?: () => void;
onSynced?: () => void;
};

type TCollaborativeEditorHookProps = {
Expand Down
6 changes: 4 additions & 2 deletions packages/editor/src/core/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
TFileHandler,
TServerHandler,
} from "@/types";
import { DocumentEventsServer } from "src/lib";
import { HocuspocusProvider } from "@hocuspocus/provider";

// editor refs
export type EditorReadOnlyRefApi = {
Expand All @@ -30,8 +32,8 @@ export type EditorReadOnlyRefApi = {
paragraphs: number;
words: number;
};
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (message: DocumentEventsServer) => void;
listenToRealTimeUpdate: () => HocuspocusProvider;
};

export interface EditorRefApi extends EditorReadOnlyRefApi {
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "@/extensions/core-without-props";
export * from "@/helpers/document-events";
13 changes: 10 additions & 3 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, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// document-editor
Expand All @@ -7,7 +7,6 @@ import {
CollaborativeDocumentReadOnlyEditorWithRef,
EditorReadOnlyRefApi,
EditorRefApi,
IMarking,
TAIMenuProps,
TDisplayConfig,
TRealtimeConfig,
Expand Down Expand Up @@ -57,6 +56,9 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
readOnlyEditorRef,
sidePeekVisible,
} = props;
// states
const [isSynced, setIsSynced] = useState(false);

// router
const { workspaceSlug, projectId } = useParams();
// store hooks
Expand All @@ -67,7 +69,7 @@ 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 { isContentEditable, updateTitle, setIsSubmitting } = page;
Expand Down Expand Up @@ -105,10 +107,15 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
handleConnectionStatus(true);
}, []);

const handleServerSynced = useCallback(() => {
setIsSynced(true);
}, []);

const serverHandler: TServerHandler = useMemo(
() => ({
onConnect: handleServerConnect,
onServerError: handleServerError,
onSynced: handleServerSynced,
}),
[]
);
Expand Down
144 changes: 113 additions & 31 deletions web/core/components/pages/editor/header/options-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import { observer } from "mobx-react";
import { useParams, useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react";
// document editor
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
import { DocumentEventsClient } from "@plane/editor/lib";
// ui
import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
// helpers
Expand All @@ -23,6 +25,42 @@ type Props = {

export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, page } = props;
// create a local state to track if the current action is being processed
const [localAction, setLocalAction] = useState<string | null>(null);

// listen to real time updates from the live server
useEffect(() => {
const provider = editorRef?.listenToRealTimeUpdate();

const handleStatelessMessage = (message: { payload: DocumentEventsClient }) => {
if (localAction === message.payload) {
setLocalAction(null);
return;
}

switch (message.payload) {
case "locked":
handleLockPage(false);
break;
case "unlocked":
handleUnlockPage(false);
break;
case "archived":
handleArchivePage(false);
break;
case "unarchived":
handleRestorePage(false);
break;
}
};

provider?.on("stateless", handleStatelessMessage);

return () => {
provider?.off("stateless", handleStatelessMessage);
};
}, [editorRef, localAction]);

// router
const router = useRouter();
// store values
Expand All @@ -45,41 +83,85 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
// update query params
const { updateQueryParams } = useQueryParams();

const handleArchivePage = async () =>
await archive().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be archived. Please try again later.",
const handleArchivePage = async (isLocal: boolean = true) => {
await archive()
.then(() => {
if (isLocal) {
setLocalAction("archived");
}
})
);

const handleRestorePage = async () =>
await restore().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be restored. Please try again later.",
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be archived. Please try again later.",
});
});
};

// watch for changes in localAction
useEffect(() => {
if (localAction === "archived") {
editorRef?.emitRealTimeUpdate("archive");
}
if (localAction === "unarchived") {
editorRef?.emitRealTimeUpdate("unarchive");
}
if (localAction === "locked") {
editorRef?.emitRealTimeUpdate("lock");
}
if (localAction === "unlocked") {
editorRef?.emitRealTimeUpdate("unlock");
}
}, [localAction, editorRef]);

const handleRestorePage = async (isLocal: boolean = true) => {
await restore()
.then(() => {
if (isLocal) {
setLocalAction("unarchived");
}
})
);

const handleLockPage = async () =>
await lock().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be locked. Please try again later.",
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be restored. Please try again later.",
})
);
};

const handleLockPage = async (isLocal: boolean = true) => {
await lock()
.then(() => {
if (isLocal) {
setLocalAction("locked");
}
})
);

const handleUnlockPage = async () =>
await unlock().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be unlocked. Please try again later.",
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be locked. Please try again later.",
})
);
};

const handleUnlockPage = async (isLocal: boolean = true) => {
await unlock()
.then(() => {
if (isLocal) {
setLocalAction("unlocked");
}
})
);
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be unlocked. Please try again later.",
})
);
};

// menu items list
const MENU_ITEMS: {
Expand Down
Loading