[WEB-1116] feat: pages realtime collaboration (#5493)
* [WEB-1116] feat: pages realtime sync (#5057) * init: live server for editor realtime sync * chore: authentication added * chore: updated logic to convert html to binary for old pages * chore: added description json on page update * chore: made all functions generic * chore: save description in json and html formats * refactor: document editor components * chore: uncomment ui package components * fix: without props extensions refactor * fix: merge conflicts resolved from preview * chore: init docker compose * chore: pages custom error codes * chore: add health check endpoint to the live server * chore: update without props extensions type * chore: better error handling * chore: update react-hook-form versions --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * fix: docker related fixes * fix: module type fixes * fix: nginx update * fix: adding live server workflow * fix: workflow fixes * fix: docker compose fixes * fix: workflow fixes * fix: path config * fix: docker compose warnings * fix: nginx port forwarding * fix: update docker compose with new env * fix: env var fixes * fix: error handling * fix: docker compose env var * fix: compose fixes * chore: update server start message * chore: handle errors * fix: build errors * chore: update port * chore: update server port * chore: show error on authentication fail * chore: show error on authentication fail * feat: add redis extension * chore: updated restore version logic --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
This commit is contained in:
parent
2c950713a7
commit
6c3a8a9647
71 changed files with 4135 additions and 4105 deletions
22
web/core/hooks/use-online-status.ts
Normal file
22
web/core/hooks/use-online-status.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
const useOnlineStatus = () => {
|
||||
// states
|
||||
const [isOnline, setIsOnline] = useState(typeof navigator !== "undefined" ? navigator.onLine : true);
|
||||
|
||||
const updateOnlineStatus = () => setIsOnline(navigator.onLine);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("online", updateOnlineStatus);
|
||||
window.addEventListener("offline", updateOnlineStatus);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", updateOnlineStatus);
|
||||
window.removeEventListener("offline", updateOnlineStatus);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isOnline };
|
||||
};
|
||||
|
||||
export default useOnlineStatus;
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
// plane editor
|
||||
import {
|
||||
EditorRefApi,
|
||||
proseMirrorJSONToBinaryString,
|
||||
applyUpdates,
|
||||
generateJSONfromHTMLForDocumentEditor,
|
||||
} from "@plane/editor";
|
||||
// hooks
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import useAutoSave from "@/hooks/use-auto-save";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// services
|
||||
import { ProjectPageService } from "@/services/page";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
|
||||
const projectPageService = new ProjectPageService();
|
||||
|
||||
type Props = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
page: IPage;
|
||||
projectId: string | string[] | undefined;
|
||||
workspaceSlug: string | string[] | undefined;
|
||||
};
|
||||
|
||||
export const usePageDescription = (props: Props) => {
|
||||
const { editorRef, page, projectId, workspaceSlug } = props;
|
||||
const [isDescriptionReady, setIsDescriptionReady] = useState(false);
|
||||
const [localDescriptionYJS, setLocalDescriptionYJS] = useState<Uint8Array>();
|
||||
const { isContentEditable, isSubmitting, updateDescription, setIsSubmitting } = page;
|
||||
const [hasShownOfflineToast, setHasShownOfflineToast] = useState(false);
|
||||
|
||||
const pageDescription = page.description_html;
|
||||
const pageId = page.id;
|
||||
|
||||
const { data: pageDescriptionYJS, mutate: mutateDescriptionYJS } = useSWR(
|
||||
workspaceSlug && projectId && pageId ? `PAGE_DESCRIPTION_${workspaceSlug}_${projectId}_${pageId}` : null,
|
||||
workspaceSlug && projectId && pageId
|
||||
? async () => {
|
||||
const encodedDescription = await projectPageService.fetchDescriptionYJS(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
pageId.toString()
|
||||
);
|
||||
const decodedDescription = new Uint8Array(encodedDescription);
|
||||
return decodedDescription;
|
||||
}
|
||||
: null,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
}
|
||||
);
|
||||
|
||||
// set the merged local doc by the provider to the react local state
|
||||
const handleDescriptionChange = useCallback((update: Uint8Array, source?: string) => {
|
||||
setLocalDescriptionYJS(() => {
|
||||
// handle the initial sync case where indexeddb gives extra update, in
|
||||
// this case we need to save the update to the DB
|
||||
if (source && source === "initialSync") {
|
||||
handleSaveDescription(true, update);
|
||||
}
|
||||
|
||||
return update;
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// if description_binary field is empty, convert description_html to yDoc and update the DB
|
||||
// TODO: this is a one-time operation, and needs to be removed once all the pages are updated
|
||||
useEffect(() => {
|
||||
const changeHTMLToBinary = async () => {
|
||||
if (!pageDescriptionYJS || !pageDescription) return;
|
||||
if (pageDescriptionYJS.length === 0) {
|
||||
const { contentJSON, editorSchema } = generateJSONfromHTMLForDocumentEditor(pageDescription ?? "<p></p>");
|
||||
const yDocBinaryString = proseMirrorJSONToBinaryString(contentJSON, "default", editorSchema);
|
||||
|
||||
try {
|
||||
await updateDescription(yDocBinaryString, pageDescription ?? "<p></p>");
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
}
|
||||
|
||||
await mutateDescriptionYJS();
|
||||
|
||||
setIsDescriptionReady(true);
|
||||
} else setIsDescriptionReady(true);
|
||||
};
|
||||
changeHTMLToBinary();
|
||||
}, [mutateDescriptionYJS, pageDescription, pageDescriptionYJS, updateDescription]);
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef?.current?.hasUnsyncedChanges() || isSubmitting === "submitting") {
|
||||
setShowAlert(true);
|
||||
} else {
|
||||
setShowAlert(false);
|
||||
}
|
||||
}, [setShowAlert, isSubmitting, editorRef, localDescriptionYJS]);
|
||||
|
||||
// merge the description from remote to local state and only save if there are local changes
|
||||
const handleSaveDescription = useCallback(
|
||||
async (forceSync?: boolean, initSyncVectorAsUpdate?: Uint8Array) => {
|
||||
const update = localDescriptionYJS ?? initSyncVectorAsUpdate;
|
||||
|
||||
if (update == null) return;
|
||||
|
||||
if (!isContentEditable) return;
|
||||
|
||||
const applyUpdatesAndSave = async (latestDescription: Uint8Array, update: Uint8Array | undefined) => {
|
||||
if (!workspaceSlug || !projectId || !pageId || !latestDescription || !update) return;
|
||||
|
||||
if (!forceSync && !editorRef.current?.hasUnsyncedChanges()) {
|
||||
setIsSubmitting("saved");
|
||||
return;
|
||||
}
|
||||
|
||||
const combinedBinaryString = applyUpdates(latestDescription, update);
|
||||
const descriptionHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
await updateDescription(combinedBinaryString, descriptionHTML)
|
||||
.then(() => {
|
||||
editorRef.current?.setSynced();
|
||||
setHasShownOfflineToast(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.message === "Network Error" && !hasShownOfflineToast) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Info!",
|
||||
message: "You seem to be offline, your changes will remain saved on this device",
|
||||
});
|
||||
setHasShownOfflineToast(true);
|
||||
}
|
||||
if (e.response?.status === 471) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to save your changes, the page was locked, your changes will be lost",
|
||||
});
|
||||
}
|
||||
if (e.response?.status === 472) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to save your changes, the page was archived, your changes will be lost",
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setShowAlert(false);
|
||||
setIsSubmitting("saved");
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
setIsSubmitting("submitting");
|
||||
const latestDescription = await mutateDescriptionYJS();
|
||||
if (latestDescription) {
|
||||
await applyUpdatesAndSave(latestDescription, update);
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSubmitting("saved");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[
|
||||
localDescriptionYJS,
|
||||
setShowAlert,
|
||||
editorRef,
|
||||
hasShownOfflineToast,
|
||||
isContentEditable,
|
||||
mutateDescriptionYJS,
|
||||
pageId,
|
||||
projectId,
|
||||
setIsSubmitting,
|
||||
updateDescription,
|
||||
workspaceSlug,
|
||||
]
|
||||
);
|
||||
|
||||
const manuallyUpdateDescription = async (descriptionHTML: string) => {
|
||||
const { contentJSON, editorSchema } = generateJSONfromHTMLForDocumentEditor(descriptionHTML ?? "<p></p>");
|
||||
const yDocBinaryString = proseMirrorJSONToBinaryString(contentJSON, "default", editorSchema);
|
||||
|
||||
try {
|
||||
editorRef.current?.clearEditor(true);
|
||||
await updateDescription(yDocBinaryString, descriptionHTML ?? "<p></p>");
|
||||
await mutateDescriptionYJS();
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
useAutoSave(handleSaveDescription);
|
||||
|
||||
return {
|
||||
handleDescriptionChange,
|
||||
isDescriptionReady,
|
||||
pageDescriptionYJS,
|
||||
handleSaveDescription,
|
||||
manuallyUpdateDescription,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue