feat: pages collaboration

This commit is contained in:
Aaryan Khandelwal 2024-05-11 18:16:49 +05:30
parent 6f190ea6ee
commit 17c539658b
13 changed files with 353 additions and 69 deletions

View File

@ -430,17 +430,17 @@ class PagesDescriptionViewSet(BaseViewSet):
if page.description_yjs:
Y.apply_update(existing_doc, page.description_yjs)
# Load the new data into a separate YDoc
new_doc = Y.YDoc()
Y.apply_update(new_doc, new_binary_data)
# # Load the new data into a separate YDoc
# new_doc = Y.YDoc()
Y.apply_update(existing_doc, new_binary_data)
# Merge the new data into the existing data
# This will automatically resolve any conflicts
new_state_vector = Y.encode_state_vector(new_doc)
diff = Y.encode_state_as_update(existing_doc, new_state_vector)
Y.apply_update(existing_doc, diff)
# # Merge the new data into the existing data
# # This will automatically resolve any conflicts
# new_state_vector = Y.encode_state_vector(new_doc)
# diff = Y.encode_state_as_update(existing_doc, new_state_vector)
# Y.apply_update(existing_doc, diff)
# Encode the updated state as binary data
# # Encode the updated state as binary data
updated_binary_data = Y.encode_state_as_update(existing_doc)
# Store the updated binary data

View File

@ -12,18 +12,17 @@ import { insertContentAtSavedSelection } from "src/helpers/insert-content-at-cur
import { EditorMenuItemNames, getEditorMenuItems } from "src/ui/menus/menu-items";
import { EditorRefApi } from "src/types/editor-ref-api";
import { IMarking, scrollSummary } from "src/helpers/scroll-to-node";
interface CustomEditorProps {
export interface CustomEditorProps {
id?: string;
uploadFile: UploadImage;
restoreFile: RestoreImage;
deleteFile: DeleteImage;
cancelUploadImage?: () => void;
initialValue: string;
initialValue?: string;
editorClassName: string;
// undefined when prop is not passed, null if intentionally passed to stop
// swr syncing
value: string | null | undefined;
value?: string | null | undefined;
onChange?: (json: object, html: string) => void;
extensions?: any;
editorProps?: EditorProps;

View File

@ -24,6 +24,7 @@ export * from "src/ui/menus/menu-items";
export * from "src/lib/editor-commands";
// types
export type { CustomEditorProps } from "src/hooks/use-editor";
export type { DeleteImage } from "src/types/delete-image";
export type { UploadImage } from "src/types/upload-image";
export type { EditorRefApi, EditorReadOnlyRefApi } from "src/types/editor-ref-api";

View File

@ -34,12 +34,17 @@
"@plane/ui": "*",
"@tippyjs/react": "^4.2.6",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-collaboration": "^2.3.2",
"@tiptap/pm": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"lucide-react": "^0.378.0",
"react-popper": "^2.3.0",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.5",
"y-protocols": "^1.0.6",
"yjs": "^13.6.15"
},
"devDependencies": {
"@types/node": "18.15.3",

View File

@ -0,0 +1,101 @@
import { useLayoutEffect, useMemo } from "react";
import {
DeleteImage,
EditorRefApi,
IMentionHighlight,
IMentionSuggestion,
RestoreImage,
UploadImage,
useEditor,
} from "@plane/editor-core";
import * as Y from "yjs";
import { CollaborationProvider } from "src/providers/collaboration-provider";
import { DocumentEditorExtensions } from "src/ui/extensions";
import { IndexeddbPersistence } from "y-indexeddb";
import { EditorProps } from "@tiptap/pm/view";
type DocumentEditorProps = {
id?: string;
uploadFile: UploadImage;
restoreFile: RestoreImage;
deleteFile: DeleteImage;
cancelUploadImage?: () => void;
value: Uint8Array;
editorClassName: string;
onChange: (binaryString: string, html: string) => void;
extensions?: any;
editorProps?: EditorProps;
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>;
suggestions?: () => Promise<IMentionSuggestion[]>;
};
handleEditorReady?: (value: boolean) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void;
tabIndex?: number;
};
export const useDocumentEditor = ({
uploadFile,
id = "",
deleteFile,
cancelUploadImage,
editorProps = {},
value,
editorClassName,
onChange,
forwardedRef,
tabIndex,
restoreFile,
handleEditorReady,
mentionHandler,
placeholder,
setHideDragHandleFunction,
}: DocumentEditorProps) => {
const provider = useMemo(
() =>
new CollaborationProvider({
name: id,
onChange,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[id]
);
const yDoc = useMemo(() => {
if (value.byteLength !== 0) Y.applyUpdate(provider.document, value);
return provider.document;
}, [value, provider.document]);
console.log("yDoc", yDoc);
// indexedDB provider
useLayoutEffect(() => {
const localProvider = new IndexeddbPersistence(id, provider.document);
return () => {
localProvider?.destroy();
};
}, [provider, id]);
const editor = useEditor({
id,
editorProps,
editorClassName,
restoreFile,
uploadFile,
deleteFile,
cancelUploadImage,
handleEditorReady,
forwardedRef,
mentionHandler,
extensions: DocumentEditorExtensions({
uploadFile,
setHideDragHandle: setHideDragHandleFunction,
provider,
}),
placeholder,
tabIndex,
});
return editor;
};

View File

@ -0,0 +1,70 @@
import * as Y from "yjs";
export interface CompleteCollaboratorProviderConfiguration {
/**
* The identifier/name of your document
*/
name: string;
/**
* The actual Y.js document
*/
document: Y.Doc;
/**
* onChange callback
*/
onChange: (binaryString: string, html: string) => void;
}
export type CollaborationProviderConfiguration = Required<Pick<CompleteCollaboratorProviderConfiguration, "name">> &
Partial<CompleteCollaboratorProviderConfiguration>;
export class CollaborationProvider {
public configuration: CompleteCollaboratorProviderConfiguration = {
name: "",
// @ts-expect-error cannot be undefined
document: undefined,
onChange: () => {},
};
intervals: any = {
forceSync: null,
};
timeoutId: any;
constructor(configuration: CollaborationProviderConfiguration) {
this.setConfiguration(configuration);
this.timeoutId = null;
this.configuration.document = configuration.document ?? new Y.Doc();
this.document.on("update", this.documentUpdateHandler.bind(this));
}
public setConfiguration(configuration: Partial<CompleteCollaboratorProviderConfiguration> = {}): void {
this.configuration = {
...this.configuration,
...configuration,
};
}
get document() {
return this.configuration.document;
}
documentUpdateHandler(update: Uint8Array, origin: any) {
if (origin === this) return;
// debounce onChange call
if (this.timeoutId !== null) clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(() => {
const docAsUint8Array = Y.encodeStateAsUpdate(this.document);
const base64Doc = Buffer.from(docAsUint8Array).toString("base64");
// const base64Doc = Buffer.from(update).toString("base64");
this.configuration.onChange?.(base64Doc, "<p></p>");
this.timeoutId = null;
}, 2000);
}
}

View File

@ -2,14 +2,20 @@ import { IssueWidgetPlaceholder } from "src/ui/extensions/widgets/issue-embed-wi
import { SlashCommand, DragAndDrop } from "@plane/editor-extensions";
import { UploadImage } from "@plane/editor-core";
import { CollaborationProvider } from "src/providers/collaboration-provider";
import Collaboration from "@tiptap/extension-collaboration";
type TArguments = {
uploadFile: UploadImage;
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
provider: CollaborationProvider;
};
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle }: TArguments) => [
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle, provider }: TArguments) => [
SlashCommand(uploadFile),
DragAndDrop(setHideDragHandle),
IssueWidgetPlaceholder(),
Collaboration.configure({
document: provider.document,
}),
];

View File

@ -4,17 +4,16 @@ import {
DeleteImage,
RestoreImage,
getEditorClassNames,
useEditor,
EditorRefApi,
IMentionHighlight,
IMentionSuggestion,
} from "@plane/editor-core";
import { DocumentEditorExtensions } from "src/ui/extensions";
import { PageRenderer } from "src/ui/components/page-renderer";
import { useDocumentEditor } from "src/hooks/use-document-editor";
interface IDocumentEditor {
initialValue: string;
value?: string;
id: string;
value: Uint8Array;
fileHandler: {
cancel: () => void;
delete: DeleteImage;
@ -24,7 +23,7 @@ interface IDocumentEditor {
handleEditorReady?: (value: boolean) => void;
containerClassName?: string;
editorClassName?: string;
onChange: (json: object, html: string) => void;
onChange: (binaryString: string, html: string) => void;
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>;
@ -37,8 +36,9 @@ interface IDocumentEditor {
const DocumentEditor = (props: IDocumentEditor) => {
const {
onChange,
initialValue,
id,
value,
// value,
fileHandler,
containerClassName,
editorClassName = "",
@ -56,26 +56,22 @@ const DocumentEditor = (props: IDocumentEditor) => {
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
};
// use editor
const editor = useEditor({
onChange(json, html) {
onChange(json, html);
},
// use document editor
const editor = useDocumentEditor({
id,
editorClassName,
restoreFile: fileHandler.restore,
uploadFile: fileHandler.upload,
deleteFile: fileHandler.delete,
cancelUploadImage: fileHandler.cancel,
initialValue,
value,
onChange,
handleEditorReady,
forwardedRef,
mentionHandler,
extensions: DocumentEditorExtensions({
uploadFile: fileHandler.upload,
setHideDragHandle: setHideDragHandleFunction,
}),
placeholder,
setHideDragHandleFunction,
tabIndex,
});

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { Control, Controller } from "react-hook-form";
@ -22,6 +22,8 @@ import { usePageFilters } from "@/hooks/use-page-filters";
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// services
import { FileService } from "@/services/file.service";
import { PageService } from "@/services/page.service";
const pageService = new PageService();
// store
import { IPageStore } from "@/store/pages/page.store";
@ -31,7 +33,6 @@ type Props = {
control: Control<TPage, any>;
editorRef: React.RefObject<EditorRefApi>;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
swrPageDetails: TPage | undefined;
handleSubmit: () => void;
markings: IMarking[];
pageStore: IPageStore;
@ -49,12 +50,13 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
editorRef,
markings,
readOnlyEditorRef,
handleSubmit,
// handleSubmit,
pageStore,
swrPageDetails,
sidePeekVisible,
updateMarkings,
} = props;
// states
const [descriptionYJS, setDescriptionYJS] = useState<Uint8Array | null>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -67,6 +69,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
} = useMember();
// derived values
const workspaceId = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "" : "";
const pageId = pageStore?.id ?? "";
const pageTitle = pageStore?.name ?? "";
const pageDescription = pageStore?.description_html;
const { description_html, isContentEditable, updateTitle, isSubmitting, setIsSubmitting } = pageStore;
@ -82,13 +85,70 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
// page filters
const { isFullWidth } = usePageFilters();
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
useReloadConfirmations(isSubmitting === "submitting");
// const { data: pageDescriptionYJS } = useSWR(
// workspaceSlug && projectId && pageId ? `PAGE_DESCRIPTION_${workspaceSlug}_${projectId}_${pageId}` : null,
// workspaceSlug && projectId && pageId
// ? () => pageService.fetchDescriptionYJS(workspaceSlug.toString(), projectId.toString(), pageId.toString())
// : null
// );
const handleDescriptionChange = useCallback(
(binaryString: string, descriptionHTML: string) => {
if (!workspaceSlug || !projectId || !pageId) return;
pageService.updateDescriptionYJS(workspaceSlug.toString(), projectId.toString(), pageId.toString(), {
description_yjs: binaryString,
description_html: descriptionHTML,
});
// setIsSubmitting("submitting");
// setShowAlert(true);
// onChange(description_html);
// handleSubmit();
},
[pageId, projectId, workspaceSlug]
);
useEffect(() => {
const fetchDescription = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
console.log("fetching...");
const response = await fetch(
`http://localhost:8000/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`,
{
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/octet-stream",
},
}
);
const data = await response.arrayBuffer();
setDescriptionYJS(new Uint8Array(data));
// __AUTO_GENERATED_PRINT_VAR_START__
console.log("fetchById data: %s", data); // __AUTO_GENERATED_PRINT_VAR_END__
// if (data.byteLength === 0) {
// const yjs = await fetchByIdIfExists(workspaceSlug, projectId, pageId);
// if (yjs) {
// console.log("not found in db:", yjs, yjs instanceof Uint8Array);
// return yjs;
// }
// }
};
const interval = setInterval(() => {
fetchDescription();
}, 15000);
return () => clearInterval(interval);
}, [pageId, projectId, workspaceSlug]);
useEffect(() => {
updateMarkings(description_html ?? "<p></p>");
}, [description_html, updateMarkings]);
if (pageDescription === undefined) return <PageContentLoader />;
if (pageDescription === undefined || pageId === undefined || !descriptionYJS) return <PageContentLoader />;
return (
<div className="flex items-center h-full w-full overflow-y-auto">
@ -125,8 +185,9 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
<Controller
name="description_html"
control={control}
render={({ field: { onChange } }) => (
render={() => (
<DocumentEditorWithRef
id={pageId}
fileHandler={{
cancel: fileService.cancelUpload,
delete: fileService.getDeleteImageFunction(workspaceId),
@ -134,17 +195,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
upload: fileService.getUploadFileFunction(workspaceSlug as string, setIsSubmitting),
}}
handleEditorReady={handleEditorReady}
initialValue={pageDescription ?? "<p></p>"}
value={swrPageDetails?.description_html ?? "<p></p>"}
value={descriptionYJS}
ref={editorRef}
containerClassName="p-0 pb-64"
editorClassName="lg:px-10 pl-8"
onChange={(_description_json, description_html) => {
setIsSubmitting("submitting");
setShowAlert(true);
onChange(description_html);
handleSubmit();
}}
onChange={handleDescriptionChange}
mentionHandler={{
highlights: mentionHighlights,
suggestions: mentionSuggestions,

View File

@ -50,7 +50,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
// fetching page details
const {
data: swrPageDetails,
isValidating,
error: pageDetailsError,
} = useSWR(pageId ? `PAGE_DETAILS_${pageId}` : null, pageId ? () => getPageById(pageId.toString()) : null, {
@ -145,7 +144,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
/>
)}
<PageEditorBody
swrPageDetails={swrPageDetails}
control={control}
editorRef={editorRef}
handleEditorReady={(val) => setEditorReady(val)}

View File

@ -31,8 +31,11 @@ export abstract class APIService {
);
}
get(url: string, params = {}) {
return this.axiosInstance.get(url, params);
get(url: string, params = {}, config = {}) {
return this.axiosInstance.get(url, {
...params,
...config,
});
}
post(url: string, data = {}, config = {}) {

View File

@ -119,4 +119,24 @@ export class PageService extends APIService {
throw error?.response?.data;
});
}
async fetchDescriptionYJS(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
headers: {
"Content-Type": "application/octet-stream",
},
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async updateDescriptionYJS(workspaceSlug: string, projectId: string, pageId: string, data: any): Promise<any> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}

View File

@ -2410,6 +2410,11 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.3.1.tgz#cea919becab684688819b29481a5c43ee1ee9c52"
integrity sha512-bVX0EnDZoRXnoA7dyoZe7w2gdRjxmFEcsatHLkcr3R3x4k9oSgZXLe1C2jGbjJWr4j32tYXZ1cpKte6f1WUKzg==
"@tiptap/extension-collaboration@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-collaboration/-/extension-collaboration-2.3.2.tgz#0780eabbe2e72665ed83f86dc70790589d1d0ff1"
integrity sha512-1vN+crj5KgqoJhDV+CrfIrBWDIjfpVxiEWHBk+yQU/G2vmyQfbN/R/5gH6rOw5GT3mHqgWFtCDJo4+H/2Ete4w==
"@tiptap/extension-document@^2.3.1":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.3.1.tgz#c2c3a1d1f87e262872012508555eda8227a3bc7a"
@ -2757,7 +2762,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.2.48", "@types/react@^18.2.42", "@types/react@^18.2.48":
"@types/react@*", "@types/react@^18.2.42", "@types/react@^18.2.48":
version "18.2.48"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1"
integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==
@ -5635,6 +5640,11 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
isomorphic.js@^0.2.4:
version "0.2.5"
resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88"
integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==
iterator.prototype@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0"
@ -5845,6 +5855,13 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lib0@^0.2.42, lib0@^0.2.74, lib0@^0.2.85, lib0@^0.2.86:
version "0.2.93"
resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.93.tgz#95487c2a97657313cb1d91fbcf9f6d64b7fcd062"
integrity sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==
dependencies:
isomorphic.js "^0.2.4"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
@ -7954,16 +7971,8 @@ streamx@^2.15.0, streamx@^2.16.1:
optionalDependencies:
bare-events "^2.2.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -8043,14 +8052,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -9137,6 +9139,27 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
y-indexeddb@^9.0.12:
version "9.0.12"
resolved "https://registry.yarnpkg.com/y-indexeddb/-/y-indexeddb-9.0.12.tgz#73657f31d52886d7532256610babf5cca4ad5e58"
integrity sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==
dependencies:
lib0 "^0.2.74"
y-prosemirror@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/y-prosemirror/-/y-prosemirror-1.2.5.tgz#c448f80a6017190bc69a30a33f3930e9924fad3a"
integrity sha512-T/JATxC8P2Dbvq/dAiaiztD1a8KEwRP8oLRlT8YlaZdNlLGE1Ea0IJ8If25UlDYmk+4+uqLbqT/S+dzUmwwgbA==
dependencies:
lib0 "^0.2.42"
y-protocols@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.6.tgz#66dad8a95752623443e8e28c0e923682d2c0d495"
integrity sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==
dependencies:
lib0 "^0.2.85"
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
@ -9152,6 +9175,13 @@ yaml@^2.3.4:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362"
integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==
yjs@^13.6.15:
version "13.6.15"
resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.15.tgz#5a2402632aabf83e5baf56342b4c82fe40859306"
integrity sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==
dependencies:
lib0 "^0.2.86"
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"