[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:
parent
c919435598
commit
99184371f7
16 changed files with 444 additions and 201 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { IndexeddbPersistence } from "y-indexeddb";
|
||||
import * as Y from "yjs";
|
||||
|
||||
export interface CompleteCollaboratorProviderConfiguration {
|
||||
|
|
@ -12,7 +13,7 @@ export interface CompleteCollaboratorProviderConfiguration {
|
|||
/**
|
||||
* onChange callback
|
||||
*/
|
||||
onChange: (updates: Uint8Array) => void;
|
||||
onChange: (updates: Uint8Array, source?: string) => void;
|
||||
/**
|
||||
* Whether connection to the database has been established and all available content has been loaded or not.
|
||||
*/
|
||||
|
|
@ -25,20 +26,28 @@ export type CollaborationProviderConfiguration = Required<Pick<CompleteCollabora
|
|||
export class CollaborationProvider {
|
||||
public configuration: CompleteCollaboratorProviderConfiguration = {
|
||||
name: "",
|
||||
// @ts-expect-error cannot be undefined
|
||||
document: undefined,
|
||||
document: new Y.Doc(),
|
||||
onChange: () => {},
|
||||
hasIndexedDBSynced: false,
|
||||
};
|
||||
|
||||
unsyncedChanges = 0;
|
||||
|
||||
private initialSync = false;
|
||||
|
||||
constructor(configuration: CollaborationProviderConfiguration) {
|
||||
this.setConfiguration(configuration);
|
||||
|
||||
this.configuration.document = configuration.document ?? new Y.Doc();
|
||||
this.indexeddbProvider = new IndexeddbPersistence(`page-${this.configuration.name}`, this.document);
|
||||
this.indexeddbProvider.on("synced", () => {
|
||||
this.configuration.hasIndexedDBSynced = true;
|
||||
});
|
||||
this.document.on("update", this.documentUpdateHandler.bind(this));
|
||||
this.document.on("destroy", this.documentDestroyHandler.bind(this));
|
||||
}
|
||||
|
||||
private indexeddbProvider: IndexeddbPersistence;
|
||||
|
||||
public setConfiguration(configuration: Partial<CompleteCollaboratorProviderConfiguration> = {}): void {
|
||||
this.configuration = {
|
||||
...this.configuration,
|
||||
|
|
@ -50,17 +59,49 @@ export class CollaborationProvider {
|
|||
return this.configuration.document;
|
||||
}
|
||||
|
||||
setHasIndexedDBSynced(hasIndexedDBSynced: boolean) {
|
||||
this.configuration.hasIndexedDBSynced = hasIndexedDBSynced;
|
||||
public hasUnsyncedChanges(): boolean {
|
||||
return this.unsyncedChanges > 0;
|
||||
}
|
||||
|
||||
documentUpdateHandler(update: Uint8Array, origin: any) {
|
||||
if (!this.configuration.hasIndexedDBSynced) return;
|
||||
private resetUnsyncedChanges() {
|
||||
this.unsyncedChanges = 0;
|
||||
}
|
||||
|
||||
private incrementUnsyncedChanges() {
|
||||
this.unsyncedChanges += 1;
|
||||
}
|
||||
|
||||
public setSynced() {
|
||||
this.resetUnsyncedChanges();
|
||||
}
|
||||
|
||||
public async hasIndexedDBSynced() {
|
||||
await this.indexeddbProvider.whenSynced;
|
||||
return this.configuration.hasIndexedDBSynced;
|
||||
}
|
||||
|
||||
async documentUpdateHandler(_update: Uint8Array, origin: any) {
|
||||
await this.indexeddbProvider.whenSynced;
|
||||
|
||||
// return if the update is from the provider itself
|
||||
if (origin === this) return;
|
||||
|
||||
// call onChange with the update
|
||||
this.configuration.onChange?.(update);
|
||||
const stateVector = Y.encodeStateAsUpdate(this.document);
|
||||
|
||||
if (!this.initialSync) {
|
||||
this.configuration.onChange?.(stateVector, "initialSync");
|
||||
this.initialSync = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.configuration.onChange?.(stateVector);
|
||||
this.incrementUnsyncedChanges();
|
||||
}
|
||||
|
||||
getUpdateFromIndexedDB(): Uint8Array {
|
||||
const update = Y.encodeStateAsUpdate(this.document);
|
||||
return update;
|
||||
}
|
||||
|
||||
documentDestroyHandler() {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
|||
};
|
||||
|
||||
// use document editor
|
||||
const editor = useDocumentEditor({
|
||||
const { editor, isIndexedDbSynced } = useDocumentEditor({
|
||||
id,
|
||||
editorClassName,
|
||||
embedHandler,
|
||||
|
|
@ -74,7 +74,7 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
|||
containerClassName,
|
||||
});
|
||||
|
||||
if (!editor) return null;
|
||||
if (!editor || !isIndexedDbSynced) return null;
|
||||
|
||||
return (
|
||||
<PageRenderer
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect, useLayoutEffect, useMemo } from "react";
|
||||
import { useLayoutEffect, useMemo, useState } from "react";
|
||||
import Collaboration from "@tiptap/extension-collaboration";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { IndexeddbPersistence } from "y-indexeddb";
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import { DragAndDrop, IssueWidget } from "@/extensions";
|
||||
|
|
@ -62,21 +61,27 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
|||
[id]
|
||||
);
|
||||
|
||||
// update document on value change
|
||||
useEffect(() => {
|
||||
if (value.byteLength > 0) Y.applyUpdate(provider.document, value);
|
||||
}, [value, provider.document]);
|
||||
const [isIndexedDbSynced, setIndexedDbIsSynced] = useState(false);
|
||||
|
||||
// indexedDB provider
|
||||
// update document on value change from server
|
||||
useLayoutEffect(() => {
|
||||
const localProvider = new IndexeddbPersistence(id, provider.document);
|
||||
localProvider.on("synced", () => {
|
||||
provider.setHasIndexedDBSynced(true);
|
||||
});
|
||||
if (value.length > 0) {
|
||||
Y.applyUpdate(provider.document, value);
|
||||
}
|
||||
}, [value, provider.document, id]);
|
||||
|
||||
// watch for indexedDb to complete syncing, only after which the editor is
|
||||
// rendered
|
||||
useLayoutEffect(() => {
|
||||
async function checkIndexDbSynced() {
|
||||
const hasSynced = await provider.hasIndexedDBSynced();
|
||||
setIndexedDbIsSynced(hasSynced);
|
||||
}
|
||||
checkIndexDbSynced();
|
||||
return () => {
|
||||
localProvider?.destroy();
|
||||
setIndexedDbIsSynced(false);
|
||||
};
|
||||
}, [provider, id]);
|
||||
}, [provider]);
|
||||
|
||||
const editor = useEditor({
|
||||
id,
|
||||
|
|
@ -101,8 +106,9 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
|||
}),
|
||||
],
|
||||
placeholder,
|
||||
provider,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
return editor;
|
||||
return { editor, isIndexedDbSynced };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
|||
import { CoreEditorProps } from "@/props";
|
||||
// types
|
||||
import { DeleteImage, EditorRefApi, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types";
|
||||
import { CollaborationProvider } from "@/plane-editor/providers";
|
||||
|
||||
export type TFileHandler = {
|
||||
cancel: () => void;
|
||||
|
|
@ -29,6 +30,7 @@ export interface CustomEditorProps {
|
|||
// undefined when prop is not passed, null if intentionally passed to stop
|
||||
// swr syncing
|
||||
value?: string | null | undefined;
|
||||
provider?: CollaborationProvider;
|
||||
onChange?: (json: object, html: string) => void;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
|
|
@ -54,6 +56,7 @@ export const useEditor = ({
|
|||
forwardedRef,
|
||||
tabIndex,
|
||||
handleEditorReady,
|
||||
provider,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
}: CustomEditorProps) => {
|
||||
|
|
@ -187,6 +190,18 @@ export const useEditor = ({
|
|||
if (!editorRef.current) return;
|
||||
scrollSummary(editorRef.current, marking);
|
||||
},
|
||||
setSynced: () => {
|
||||
if (provider) {
|
||||
provider.setSynced();
|
||||
}
|
||||
},
|
||||
hasUnsyncedChanges: () => {
|
||||
if (provider) {
|
||||
return provider.hasUnsyncedChanges();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isEditorReadyToDiscard: () => editorRef.current?.storage.image.uploadInProgress === false,
|
||||
setFocusAtPosition: (position: number) => {
|
||||
if (!editorRef.current || editorRef.current.isDestroyed) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
|||
onStateChange: (callback: () => void) => () => void;
|
||||
setFocusAtPosition: (position: number) => void;
|
||||
isEditorReadyToDiscard: () => boolean;
|
||||
setSynced: () => void;
|
||||
hasUnsyncedChanges: () => boolean;
|
||||
}
|
||||
|
||||
export interface IEditorProps {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue