mirror of
https://github.com/makeplane/plane.git
synced 2025-12-23 23:29:37 +01:00
* chore: project page version * feat: page version history implemented * chore: hide save button when version history overlay is active * refactor: updated navigation logic * chore: added error states --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
208 lines
7.0 KiB
TypeScript
208 lines
7.0 KiB
TypeScript
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,
|
|
};
|
|
};
|