[WIKI-400] feat: page navigation pane (#7206)

* init: page navigation pane

* chore: outline and info tabs

* chore: asset download endpoint

* chore: realtime document info updates

* chore: add support for code splitting

* fix: formatting

* refactor: image block id generation

* chore: implement translation

* refactor: assets list storage logic

* fix: build errors

* fix: image extension name

* refactor: add support for additional asset items

* refactor: asset extraction logic

* chore: add translations

* fix: merge conflicts resolved from preview

* chore: remove version history option from the dropdown

* chore: query params handling

* chore: remove unnecessary logic

* refactor: empty state components

* fix: empty state asset path
This commit is contained in:
Aaryan Khandelwal 2025-07-02 15:25:52 +05:30 committed by GitHub
parent cfe169c6d7
commit 0b159c4963
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 2185 additions and 767 deletions

View file

@ -0,0 +1,6 @@
// helpers
import { TAssetMetaDataRecord } from "@/helpers/assets";
// local imports
import { ADDITIONAL_EXTENSIONS } from "./extensions";
export const ADDITIONAL_ASSETS_META_DATA_RECORD: Partial<Record<ADDITIONAL_EXTENSIONS, TAssetMetaDataRecord>> = {};

View file

@ -0,0 +1 @@
export enum ADDITIONAL_EXTENSIONS {}

View file

@ -0,0 +1 @@
export type TAdditionalEditorAsset = never;

View file

@ -1,3 +1,4 @@
import { CharacterCountStorage } from "@tiptap/extension-character-count";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// extensions
@ -15,6 +16,7 @@ export type ExtensionStorageMap = {
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
[CORE_EXTENSIONS.MENTION]: MentionExtensionStorage;
[CORE_EXTENSIONS.UTILITY]: UtilityExtensionStorage;
[CORE_EXTENSIONS.CHARACTER_COUNT]: CharacterCountStorage;
};
export type ExtensionFileSetStorageKey = Extract<keyof ImageExtensionStorage, "deletedImageSet">;

View file

@ -17,8 +17,6 @@ import { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types";
const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> = (props) => {
const {
onChange,
onTransaction,
aiHandler,
bubbleMenuEnabled = true,
containerClassName,
@ -33,6 +31,9 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
handleEditorReady,
id,
mentionHandler,
onAssetChange,
onChange,
onTransaction,
placeholder,
realtimeConfig,
serverHandler,
@ -63,6 +64,7 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
handleEditorReady,
id,
mentionHandler,
onAssetChange,
onChange,
onTransaction,
placeholder,

View file

@ -4,7 +4,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from
import { cn } from "@plane/utils";
// local imports
import { Pixel, TCustomImageAttributes, TCustomImageSize } from "../types";
import { ensurePixelString } from "../utils";
import { ensurePixelString, getImageBlockId } from "../utils";
import type { CustomImageNodeViewProps } from "./node-view";
import { ImageToolbarRoot } from "./toolbar";
import { ImageUploadStatus } from "./upload-status";
@ -196,6 +196,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
return (
<div
id={getImageBlockId(node.attrs.id ?? "")}
ref={containerRef}
className="group/image-component relative inline-block max-w-full"
onMouseDown={handleImageMouseDown}

View file

@ -165,7 +165,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
}
return "Add an image";
}, [draggedInside, failedToLoadImage, isImageBeingUploaded, editor.isEditable]);
}, [draggedInside, editor.isEditable, failedToLoadImage, isImageBeingUploaded]);
return (
<div

View file

@ -31,3 +31,5 @@ export const ensurePixelString = <TDefault>(
return value;
};
export const getImageBlockId = (id: string) => `editor-image-block-${id}`;

View file

@ -1,5 +1,4 @@
import { Extension } from "@tiptap/core";
// prosemirror plugins
import codemark from "prosemirror-codemark";
// helpers
import { restorePublicImages } from "@/helpers/image-helpers";
@ -8,17 +7,27 @@ import { DropHandlerPlugin } from "@/plugins/drop";
import { FilePlugins } from "@/plugins/file/root";
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
// types
import type { IEditorProps, TFileHandler, TReadOnlyFileHandler } from "@/types";
import type { IEditorProps, TEditorAsset, TFileHandler, TReadOnlyFileHandler } from "@/types";
declare module "@tiptap/core" {
interface Commands {
utility: {
updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
updateAssetsList: (
args:
| {
asset: TEditorAsset;
}
| {
idToRemove: string;
}
) => () => void;
};
}
}
export interface UtilityExtensionStorage {
assetsList: TEditorAsset[];
assetsUploadStatus: TFileHandler["assetsUploadStatus"];
uploadInProgress: boolean;
}
@ -58,6 +67,7 @@ export const UtilityExtension = (props: Props) => {
addStorage() {
return {
assetsList: [],
assetsUploadStatus: isEditable && "assetsUploadStatus" in fileHandler ? fileHandler.assetsUploadStatus : {},
uploadInProgress: false,
};
@ -68,6 +78,21 @@ export const UtilityExtension = (props: Props) => {
updateAssetsUploadStatus: (updatedStatus) => () => {
this.storage.assetsUploadStatus = updatedStatus;
},
updateAssetsList: (args) => () => {
const uniqueAssets = new Set(this.storage.assetsList);
if ("asset" in args) {
const alreadyExists = this.storage.assetsList.find((asset) => asset.id === args.asset.id);
if (!alreadyExists) {
uniqueAssets.add(args.asset);
}
} else if ("idToRemove" in args) {
const asset = this.storage.assetsList.find((asset) => asset.id === args.idToRemove);
if (asset) {
uniqueAssets.delete(asset);
}
}
this.storage.assetsList = Array.from(uniqueAssets);
},
};
},
});

View file

@ -0,0 +1,37 @@
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// extensions
import { getImageBlockId } from "@/extensions/custom-image/utils";
// plane editor imports
import { ADDITIONAL_ASSETS_META_DATA_RECORD } from "@/plane-editor/constants/assets";
// types
import { TEditorAsset } from "@/types";
export type TAssetMetaDataRecord = (attrs: ProseMirrorNode["attrs"]) => TEditorAsset | undefined;
export const CORE_ASSETS_META_DATA_RECORD: Partial<Record<CORE_EXTENSIONS, TAssetMetaDataRecord>> = {
[CORE_EXTENSIONS.IMAGE]: (attrs) => {
if (!attrs?.src) return;
return {
href: `#${getImageBlockId(attrs?.id ?? "")}`,
id: attrs?.id,
name: `image-${attrs?.id}`,
size: 0,
src: attrs?.src,
type: CORE_EXTENSIONS.IMAGE,
};
},
[CORE_EXTENSIONS.CUSTOM_IMAGE]: (attrs) => {
if (!attrs?.src) return;
return {
href: `#${getImageBlockId(attrs?.id ?? "")}`,
id: attrs?.id,
name: `image-${attrs?.id}`,
size: 0,
src: attrs?.src,
type: CORE_EXTENSIONS.CUSTOM_IMAGE,
};
},
...ADDITIONAL_ASSETS_META_DATA_RECORD,
};

View file

@ -0,0 +1,55 @@
import { HocuspocusProvider } from "@hocuspocus/provider";
import { Editor } from "@tiptap/core";
import * as Y from "yjs";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
import { CORE_EDITOR_META } from "@/constants/meta";
// types
import { EditorReadOnlyRefApi } from "@/types";
// local imports
import { getParagraphCount } from "./common";
import { getExtensionStorage } from "./get-extension-storage";
import { scrollSummary } from "./scroll-to-node";
type TArgs = {
editor: Editor | null;
provider: HocuspocusProvider | undefined;
};
export const getEditorRefHelpers = (args: TArgs): EditorReadOnlyRefApi => {
const { editor, provider } = args;
return {
clearEditor: (emitUpdate = false) => {
editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run();
},
getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editor?.getJSON() ?? null;
return {
binary: documentBinary,
html: documentHTML,
json: documentJSON,
};
},
getDocumentInfo: () => ({
characters: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.characters?.() : 0,
paragraphs: getParagraphCount(editor?.state),
words: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.words?.() : 0,
}),
getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []),
getMarkDown: () => {
const markdownOutput = editor?.storage?.markdown?.getMarkdown?.();
return markdownOutput;
},
scrollSummary: (marking) => {
if (!editor) return;
scrollSummary(editor, marking);
},
setEditorValue: (content, emitUpdate = false) => {
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true });
},
};
};

View file

@ -13,6 +13,7 @@ import { TCollaborativeEditorHookProps } from "@/types";
export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) => {
const {
onAssetChange,
onChange,
onTransaction,
disabledExtensions,
@ -106,6 +107,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
forwardedRef,
handleEditorReady,
mentionHandler,
onAssetChange,
onChange,
onTransaction,
placeholder,

View file

@ -1,23 +1,23 @@
import { DOMSerializer } from "@tiptap/pm/model";
import { useEditor as useTiptapEditor } from "@tiptap/react";
import { useEditorState, useEditor as useTiptapEditor } from "@tiptap/react";
import { useImperativeHandle, useEffect } from "react";
import * as Y from "yjs";
// components
import { getEditorMenuItems } from "@/components/menus";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
import { CORE_EDITOR_META } from "@/constants/meta";
// extensions
import { CoreEditorExtensions } from "@/extensions";
// helpers
import { getParagraphCount } from "@/helpers/common";
import { getEditorRefHelpers } from "@/helpers/editor-ref";
import { getExtensionStorage } from "@/helpers/get-extension-storage";
import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position";
import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node";
import { scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node";
// props
import { CoreEditorProps } from "@/props";
// types
import type { TDocumentEventsServer, TEditorCommands, TEditorHookProps } from "@/types";
import type { TEditorCommands, TEditorHookProps } from "@/types";
export const useEditor = (props: TEditorHookProps) => {
const {
@ -35,6 +35,7 @@ export const useEditor = (props: TEditorHookProps) => {
id = "",
initialValue,
mentionHandler,
onAssetChange,
onChange,
onTransaction,
placeholder,
@ -109,27 +110,26 @@ export const useEditor = (props: TEditorHookProps) => {
editor.commands.updateAssetsUploadStatus?.(assetsUploadStatus);
}, [editor, fileHandler.assetsUploadStatus]);
// subscribe to assets list changes
const assetsList = useEditorState({
editor,
selector: ({ editor }) => ({
assets: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.assetsList : [],
}),
});
// trigger callback when assets list changes
useEffect(() => {
const assets = assetsList?.assets;
if (!assets || !onAssetChange) return;
onAssetChange(assets);
}, [assetsList?.assets, onAssetChange]);
useImperativeHandle(
forwardedRef,
() => ({
...getEditorRefHelpers({ editor, provider }),
blur: () => editor?.commands.blur(),
scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) {
const resolvedPos = pos ?? editor?.state.selection.from;
if (!editor || !resolvedPos) return;
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
},
getCurrentCursorPosition: () => editor?.state.selection.from,
clearEditor: (emitUpdate = false) => {
editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run();
},
setEditorValue: (content: string, emitUpdate = false) => {
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true });
},
setEditorValueAtCursorPosition: (content: string) => {
if (editor?.state.selection) {
insertContentAtSavedSelection(editor, content);
}
},
emitRealTimeUpdate: (message) => provider?.sendStateless(message),
executeMenuItemCommand: (props) => {
const { itemKey } = props;
const editorItems = getEditorMenuItems(editor);
@ -143,83 +143,7 @@ export const useEditor = (props: TEditorHookProps) => {
console.warn(`No command found for item: ${itemKey}`);
}
},
isMenuItemActive: (props) => {
const { itemKey } = props;
const editorItems = getEditorMenuItems(editor);
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
const item = getEditorMenuItem(itemKey);
if (!item) return false;
return item.isActive(props);
},
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension
editor?.on("update", () => {
const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings;
if (headings) {
callback(headings);
}
});
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editor?.off("update");
};
},
getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []),
onStateChange: (callback: () => void) => {
// Subscribe to editor state changes
editor?.on("transaction", () => {
callback();
});
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editor?.off("transaction");
};
},
getMarkDown: (): string => {
const markdownOutput = editor?.storage.markdown.getMarkdown();
return markdownOutput;
},
getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editor?.getJSON() ?? null;
return {
binary: documentBinary,
html: documentHTML,
json: documentJSON,
};
},
scrollSummary: (marking: IMarking): void => {
if (!editor) return;
scrollSummary(editor, marking);
},
isEditorReadyToDiscard: () =>
!!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false,
setFocusAtPosition: (position: number) => {
if (!editor || editor.isDestroyed) {
console.error("Editor reference is not available or has been destroyed.");
return;
}
try {
const docSize = editor.state.doc.content.size;
const safePosition = Math.max(0, Math.min(position, docSize));
editor
.chain()
.insertContentAt(safePosition, [{ type: CORE_EXTENSIONS.PARAGRAPH }])
.focus()
.run();
} catch (error) {
console.error("An error occurred while setting focus at position:", error);
}
},
getCurrentCursorPosition: () => editor?.state.selection.from,
getSelectedText: () => {
if (!editor) return null;
@ -253,18 +177,99 @@ export const useEditor = (props: TEditorHookProps) => {
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
}
},
getDocumentInfo: () => ({
characters: editor?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editor?.state),
words: editor?.storage?.characterCount?.words?.() ?? 0,
}),
isEditorReadyToDiscard: () =>
!!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false,
isMenuItemActive: (props) => {
const { itemKey } = props;
const editorItems = getEditorMenuItems(editor);
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
const item = getEditorMenuItem(itemKey);
if (!item) return false;
return item.isActive(props);
},
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
onDocumentInfoChange: (callback) => {
const handleDocumentInfoChange = () => {
if (!editor) return;
callback({
characters: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.characters?.() : 0,
paragraphs: getParagraphCount(editor?.state),
words: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.words?.() : 0,
});
};
// Subscribe to update event emitted from character count extension
editor?.on("update", handleDocumentInfoChange);
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editor?.off("update", handleDocumentInfoChange);
};
},
onHeadingChange: (callback) => {
const handleHeadingChange = () => {
if (!editor) return;
const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings;
if (headings) {
callback(headings);
}
};
// Subscribe to update event emitted from headers extension
editor?.on("update", handleHeadingChange);
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editor?.off("update", handleHeadingChange);
};
},
onStateChange: (callback) => {
// Subscribe to editor state changes
editor?.on("transaction", callback);
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editor?.off("transaction", callback);
};
},
scrollToNodeViaDOMCoordinates(behavior, pos) {
const resolvedPos = pos ?? editor?.state.selection.from;
if (!editor || !resolvedPos) return;
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
},
setEditorValueAtCursorPosition: (content) => {
if (editor?.state.selection) {
insertContentAtSavedSelection(editor, content);
}
},
setFocusAtPosition: (position) => {
if (!editor || editor.isDestroyed) {
console.error("Editor reference is not available or has been destroyed.");
return;
}
try {
const docSize = editor.state.doc.content.size;
const safePosition = Math.max(0, Math.min(position, docSize));
editor
.chain()
.insertContentAt(safePosition, [{ type: CORE_EXTENSIONS.PARAGRAPH }])
.focus()
.run();
} catch (error) {
console.error("An error occurred while setting focus at position:", error);
}
},
setProviderDocument: (value) => {
const document = provider?.document;
if (!document) return;
Y.applyUpdate(document, value);
},
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
}),
[editor]
);

View file

@ -1,13 +1,9 @@
import { useEditor as useTiptapEditor } from "@tiptap/react";
import { useImperativeHandle, useEffect } from "react";
import * as Y from "yjs";
// constants
import { CORE_EDITOR_META } from "@/constants/meta";
// extensions
import { CoreReadOnlyEditorExtensions } from "@/extensions";
// helpers
import { getParagraphCount } from "@/helpers/common";
import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
import { getEditorRefHelpers } from "@/helpers/editor-ref";
// props
import { CoreReadOnlyEditorProps } from "@/props";
// types
@ -30,7 +26,7 @@ export const useReadOnlyEditor = (props: TReadOnlyEditorHookProps) => {
const editor = useTiptapEditor({
editable: false,
immediatelyRender: true,
immediatelyRender: false,
shouldRerenderOnTransaction: false,
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
parseOptions: { preserveWhitespace: true },
@ -63,38 +59,7 @@ export const useReadOnlyEditor = (props: TReadOnlyEditorHookProps) => {
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: true });
}, [editor, initialValue]);
useImperativeHandle(forwardedRef, () => ({
clearEditor: (emitUpdate = false) => {
editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run();
},
setEditorValue: (content: string, emitUpdate = false) => {
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true });
},
getMarkDown: (): string => {
const markdownOutput = editor?.storage.markdown.getMarkdown();
return markdownOutput;
},
getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editor?.getJSON() ?? null;
return {
binary: documentBinary,
html: documentHTML,
json: documentJSON,
};
},
scrollSummary: (marking: IMarking): void => {
if (!editor) return;
scrollSummary(editor, marking);
},
getDocumentInfo: () => ({
characters: editor.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editor.state),
words: editor.storage?.characterCount?.words?.() ?? 0,
}),
}));
useImperativeHandle(forwardedRef, () => getEditorRefHelpers({ editor, provider }));
if (!editor) {
return null;

View file

@ -170,7 +170,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp
return;
}
const scrollableParent = getScrollParent(dragHandleElement);
const scrollableParent = getScrollParent(dragHandleElement!);
if (!scrollableParent) return;
const scrollRegionUp = options.scrollThreshold.up;

View file

@ -57,6 +57,10 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand
if (!nodeFileSetDetails || !src) return;
try {
editor.storage[nodeType][nodeFileSetDetails.fileSetName]?.set(src, true);
// update assets list storage value
editor.commands.updateAssetsList?.({
idToRemove: node.attrs.id,
});
await deleteHandler(src);
} catch (error) {
console.error("Error deleting file via delete utility plugin:", error);

View file

@ -2,6 +2,8 @@ import { Editor } from "@tiptap/core";
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// helpers
import { CORE_ASSETS_META_DATA_RECORD } from "@/helpers/assets";
// plane editor imports
import { NODE_FILE_MAP } from "@/plane-editor/constants/utility";
// types
@ -42,6 +44,13 @@ export const TrackFileRestorationPlugin = (editor: Editor, restoreHandler: TFile
if (!isAValidNode) return;
if (pos < 0 || pos > newState.doc.content.size) return;
if (oldFileSources[nodeType]?.has(node.attrs.src)) return;
// update assets list storage value
const assetMetaData = CORE_ASSETS_META_DATA_RECORD[nodeType]?.(node.attrs);
if (assetMetaData) {
editor.commands.updateAssetsList?.({
asset: assetMetaData,
});
}
// if the src is just a id (private bucket), then we don't need to handle restore from here but
// only while it fails to load
if (nodeType === CORE_EXTENSIONS.CUSTOM_IMAGE && !node.attrs.src?.startsWith("http")) return;

View file

@ -0,0 +1,14 @@
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// plane editor imports
import { TAdditionalEditorAsset } from "@/plane-editor/types/asset";
export type TEditorImageAsset = {
href: string;
id: string;
name: string;
src: string;
type: CORE_EXTENSIONS.IMAGE | CORE_EXTENSIONS.CUSTOM_IMAGE;
};
export type TEditorAsset = TEditorImageAsset | TAdditionalEditorAsset;

View file

@ -1,5 +1,5 @@
import type { Extensions, JSONContent } from "@tiptap/core";
import type { Selection } from "@tiptap/pm/state";
import { Extensions, JSONContent } from "@tiptap/core";
import { Selection } from "@tiptap/pm/state";
// extension types
import type { TTextAlign } from "@/extensions";
// helpers
@ -10,6 +10,7 @@ import type {
TDisplayConfig,
TDocumentEventEmitter,
TDocumentEventsServer,
TEditorAsset,
TEmbedConfig,
TExtensions,
TFileHandler,
@ -75,41 +76,44 @@ type TCommandWithPropsWithItemKey<T extends TEditorCommands> = T extends keyof T
? { itemKey: T } & TCommandExtraProps[T]
: { itemKey: T };
export type TDocumentInfo = {
characters: number;
paragraphs: number;
words: number;
};
// editor refs
export type EditorReadOnlyRefApi = {
getMarkDown: () => string;
clearEditor: (emitUpdate?: boolean) => void;
getDocument: () => {
binary: Uint8Array | null;
html: string;
json: JSONContent | null;
};
clearEditor: (emitUpdate?: boolean) => void;
setEditorValue: (content: string, emitUpdate?: boolean) => void;
getDocumentInfo: () => TDocumentInfo;
getHeadings: () => IMarking[];
getMarkDown: () => string;
scrollSummary: (marking: IMarking) => void;
getDocumentInfo: () => {
characters: number;
paragraphs: number;
words: number;
};
setEditorValue: (content: string, emitUpdate?: boolean) => void;
};
export interface EditorRefApi extends EditorReadOnlyRefApi {
blur: () => void;
scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void;
getCurrentCursorPosition: () => number | undefined;
setEditorValueAtCursorPosition: (content: string) => void;
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
executeMenuItemCommand: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => void;
isMenuItemActive: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => boolean;
onStateChange: (callback: () => void) => () => void;
setFocusAtPosition: (position: number) => void;
isEditorReadyToDiscard: () => boolean;
getCurrentCursorPosition: () => number | undefined;
getSelectedText: () => string | null;
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
setProviderDocument: (value: Uint8Array) => void;
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
isEditorReadyToDiscard: () => boolean;
isMenuItemActive: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => boolean;
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
onDocumentInfoChange: (callback: (documentInfo: TDocumentInfo) => void) => () => void;
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
onStateChange: (callback: () => void) => () => void;
scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void;
setEditorValueAtCursorPosition: (content: string) => void;
setFocusAtPosition: (position: number) => void;
setProviderDocument: (value: Uint8Array) => void;
}
// editor props
@ -128,6 +132,7 @@ export interface IEditorProps {
id: string;
initialValue: string;
mentionHandler: TMentionHandler;
onAssetChange?: (assets: TEditorAsset[]) => void;
onChange?: (json: object, html: string) => void;
onEnterKeyPress?: (e?: any) => void;
onTransaction?: () => void;

View file

@ -18,6 +18,7 @@ export type TEditorHookProps = TCoreHookProps &
| "forwardedRef"
| "id"
| "mentionHandler"
| "onAssetChange"
| "onChange"
| "onTransaction"
| "placeholder"
@ -38,6 +39,7 @@ export type TCollaborativeEditorHookProps = TCoreHookProps &
| "forwardedRef"
| "id"
| "mentionHandler"
| "onAssetChange"
| "onChange"
| "onTransaction"
| "placeholder"

View file

@ -1,4 +1,5 @@
export * from "./ai";
export * from "./asset";
export * from "./collaboration";
export * from "./config";
export * from "./editor";

View file

@ -35,5 +35,8 @@ export { useEditor } from "@/hooks/use-editor";
export { type IMarking, useEditorMarkings } from "@/hooks/use-editor-markings";
export { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
export { CORE_EXTENSIONS } from "@/constants/extension";
export { ADDITIONAL_EXTENSIONS } from "@/plane-editor/constants/extensions";
// types
export * from "@/types";