diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index d7d3819f21d..751c791011f 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -1,3 +1,4 @@ +import { Extensions } from "@tiptap/core"; import React from "react"; // components import { DocumentContentLoader, PageRenderer } from "@/components/editors"; @@ -35,7 +36,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { user, } = props; - const extensions = []; + const extensions: Extensions = []; if (embedHandler?.issue) { extensions.push( IssueWidget({ diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx index c8753fe42fd..58e834e71d6 100644 --- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/document/read-only-editor.tsx @@ -1,3 +1,4 @@ +import { Extensions } from "@tiptap/core"; import { forwardRef, MutableRefObject } from "react"; // components import { PageRenderer } from "@/components/editors"; @@ -41,7 +42,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { initialValue, mentionHandler, } = props; - const extensions = []; + const extensions: Extensions = []; if (embedHandler?.issue) { extensions.push( IssueWidget({ diff --git a/packages/editor/src/core/components/menus/ai-menu.tsx b/packages/editor/src/core/components/menus/ai-menu.tsx index 42b45509f72..968c7d4690f 100644 --- a/packages/editor/src/core/components/menus/ai-menu.tsx +++ b/packages/editor/src/core/components/menus/ai-menu.tsx @@ -23,6 +23,7 @@ export const AIFeaturesMenu: React.FC = (props) => { menuRef.current.remove(); menuRef.current.style.visibility = "visible"; + // @ts-expect-error - Tippy types are incorrect popup.current = tippy(document.body, { getReferenceClientRect: null, content: menuRef.current, diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index 77d5ce81ec5..c143abd009c 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -34,6 +34,7 @@ export const BlockMenu = (props: BlockMenuProps) => { menuRef.current.remove(); menuRef.current.style.visibility = "visible"; + // @ts-expect-error - Tippy types are incorrect popup.current = tippy(document.body, { getReferenceClientRect: null, content: menuRef.current, diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index e87c0984b7f..8cb4accc5fa 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -218,24 +218,33 @@ export const HorizontalRuleItem = (editor: Editor) => export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({ key: "text-color", name: "Color", - isActive: ({ color }) => editor.isActive("customColor", { color }), - command: ({ color }) => toggleTextColor(color, editor), + isActive: (props) => editor.isActive("customColor", { color: props?.color }), + command: (props) => { + if (!props) return; + toggleTextColor(props.color, editor); + }, icon: Palette, }); export const BackgroundColorItem = (editor: Editor): EditorMenuItem<"background-color"> => ({ key: "background-color", name: "Background color", - isActive: ({ color }) => editor.isActive("customColor", { backgroundColor: color }), - command: ({ color }) => toggleBackgroundColor(color, editor), + isActive: (props) => editor.isActive("customColor", { backgroundColor: props?.color }), + command: (props) => { + if (!props) return; + toggleBackgroundColor(props.color, editor); + }, icon: Palette, }); export const TextAlignItem = (editor: Editor): EditorMenuItem<"text-align"> => ({ key: "text-align", name: "Text align", - isActive: ({ alignment }) => editor.isActive({ textAlign: alignment }), - command: ({ alignment }) => setTextAlign(alignment, editor), + isActive: (props) => editor.isActive({ textAlign: props?.alignment }), + command: (props) => { + if (!props) return; + setTextAlign(props.alignment, editor); + }, icon: AlignCenter, }); diff --git a/packages/editor/src/core/extensions/custom-color.ts b/packages/editor/src/core/extensions/custom-color.ts index 9353414b427..b377099fb59 100644 --- a/packages/editor/src/core/extensions/custom-color.ts +++ b/packages/editor/src/core/extensions/custom-color.ts @@ -106,6 +106,8 @@ export const CustomColorExtension = Mark.create({ }; }, + // @ts-expect-error types are incorrect + // TODO: check this and update types parseHTML() { return [ { diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 89194aae0c3..961764081e5 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -1,5 +1,5 @@ -import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react"; import { NodeSelection } from "@tiptap/pm/state"; +import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react"; // plane utils import { cn } from "@plane/utils"; // extensions @@ -38,11 +38,11 @@ const ensurePixelString = (value: Pixel | TDefault | number | undefin }; type CustomImageBlockProps = CustoBaseImageNodeViewProps & { - imageFromFileSystem: string; + imageFromFileSystem: string | undefined; setFailedToLoadImage: (isError: boolean) => void; editorContainer: HTMLDivElement | null; setEditorContainer: (editorContainer: HTMLDivElement | null) => void; - src: string; + src: string | undefined; }; export const CustomImageBlock: React.FC = (props) => { @@ -62,8 +62,8 @@ export const CustomImageBlock: React.FC = (props) => { const { width: nodeWidth, height: nodeHeight, aspectRatio: nodeAspectRatio, src: imgNodeSrc } = node.attrs; // states const [size, setSize] = useState({ - width: ensurePixelString(nodeWidth, "35%"), - height: ensurePixelString(nodeHeight, "auto"), + width: ensurePixelString(nodeWidth, "35%") ?? "35%", + height: ensurePixelString(nodeHeight, "auto") ?? "auto", aspectRatio: nodeAspectRatio || null, }); const [isResizing, setIsResizing] = useState(false); @@ -144,8 +144,8 @@ export const CustomImageBlock: React.FC = (props) => { useLayoutEffect(() => { setSize((prevSize) => ({ ...prevSize, - width: ensurePixelString(nodeWidth), - height: ensurePixelString(nodeHeight), + width: ensurePixelString(nodeWidth) ?? "35%", + height: ensurePixelString(nodeHeight) ?? "auto", aspectRatio: nodeAspectRatio, })); }, [nodeWidth, nodeHeight, nodeAspectRatio]); @@ -247,7 +247,16 @@ export const CustomImageBlock: React.FC = (props) => { try { setHasErroredOnFirstLoad(true); // this is a type error from tiptap, don't remove await until it's fixed + if (!imgNodeSrc) { + throw new Error("No source image to restore from"); + } await editor?.commands.restoreImage?.(imgNodeSrc); + if (!imageRef.current) { + throw new Error("Image reference not found"); + } + if (!resolvedImageSrc) { + throw new Error("No resolved image source available"); + } imageRef.current.src = resolvedImageSrc; } catch { // if the image failed to even restore, then show the error state @@ -277,7 +286,7 @@ export const CustomImageBlock: React.FC = (props) => { } image={{ src: resolvedImageSrc, - aspectRatio: size.aspectRatio, + aspectRatio: size.aspectRatio === null ? 1 : size.aspectRatio, height: size.height, width: size.width, }} diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index eaea423878d..03505adb64d 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -1,13 +1,13 @@ -import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react"; import { ImageIcon } from "lucide-react"; +import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react"; // plane utils import { cn } from "@plane/utils"; // constants import { ACCEPTED_FILE_EXTENSIONS } from "@/constants/config"; -// hooks -import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload"; // extensions import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image"; +// hooks +import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload"; type CustomImageUploaderProps = CustoBaseImageNodeViewProps & { maxFileSize: number; @@ -38,6 +38,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { const onUpload = useCallback( (url: string) => { if (url) { + if (!imageEntityId) return; setIsUploaded(true); // Update the node view's src attribute post upload updateAttributes({ src: url }); @@ -82,7 +83,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { // the meta data of the image component const meta = useMemo( - () => imageComponentImageFileMap?.get(imageEntityId), + () => imageComponentImageFileMap?.get(imageEntityId ?? ""), [imageComponentImageFileMap, imageEntityId] ); @@ -96,7 +97,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { if (meta.hasOpenedFileInputOnce) return; fileInputRef.current.click(); hasTriggeredFilePickerRef.current = true; - imageComponentImageFileMap?.set(imageEntityId, { ...meta, hasOpenedFileInputOnce: true }); + imageComponentImageFileMap?.set(imageEntityId ?? "", { ...meta, hasOpenedFileInputOnce: true }); } } }, [meta, uploadFile, imageComponentImageFileMap]); diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx index 560d95cfc4c..61ae307bb66 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx @@ -29,7 +29,7 @@ export const ImageFullScreenAction: React.FC = (props) => { const dragStart = useRef({ x: 0, y: 0 }); const dragOffset = useRef({ x: 0, y: 0 }); const modalRef = useRef(null); - const imgRef = useRef(null); + const imgRef = useRef(null); const widthInNumber = useMemo(() => Number(width?.replace("px", "")), [width]); diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 0b772baf9db..f04ed756a64 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -32,10 +32,10 @@ import { } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; -// types -import { TExtensions, TFileHandler, TMentionHandler } from "@/types"; // plane editor extensions import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions"; +// types +import { TExtensions, TFileHandler, TMentionHandler } from "@/types"; type TArguments = { disabledExtensions: TExtensions[]; @@ -148,7 +148,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CustomMentionExtension(mentionHandler), Placeholder.configure({ placeholder: ({ editor, node }) => { - if (!editor.isEditable) return; + if (!editor.isEditable) return ""; if (node.type.name === "heading") return `Heading ${node.attrs.level}`; diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx index 28aaebb8527..c0c078a2dda 100644 --- a/packages/editor/src/core/extensions/slash-commands/root.tsx +++ b/packages/editor/src/core/extensions/slash-commands/root.tsx @@ -69,6 +69,7 @@ const renderItems = () => { const tippyContainer = document.querySelector(".active-editor") ?? document.querySelector('[id^="editor-container"]'); + // @ts-expect-error - Tippy types are incorrect popup = tippy("body", { getReferenceClientRect: props.clientRect, appendTo: tippyContainer, diff --git a/packages/editor/src/core/extensions/typography/index.ts b/packages/editor/src/core/extensions/typography/index.ts index e9d48b41581..6b736953b53 100644 --- a/packages/editor/src/core/extensions/typography/index.ts +++ b/packages/editor/src/core/extensions/typography/index.ts @@ -1,4 +1,4 @@ -import { Extension } from "@tiptap/core"; +import { Extension, InputRule } from "@tiptap/core"; import { TypographyOptions, emDash, @@ -26,7 +26,7 @@ export const CustomTypographyExtension = Extension.create({ name: "typography", addInputRules() { - const rules = []; + const rules: InputRule[] = []; if (this.options.emDash !== false) { rules.push(emDash(this.options.emDash)); diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 23bb5088417..03e9353f4d7 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -128,13 +128,13 @@ export const useEditor = (props: CustomEditorProps) => { useImperativeHandle( forwardedRef, () => ({ - blur: () => editor.commands.blur(), + blur: () => editor?.commands.blur(), scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) { - const resolvedPos = pos ?? editor.state.selection.from; + const resolvedPos = pos ?? editor?.state.selection.from; if (!editor || !resolvedPos) return; scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior); }, - getCurrentCursorPosition: () => editor.state.selection.from, + getCurrentCursorPosition: () => editor?.state.selection.from, clearEditor: (emitUpdate = false) => { editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); }, @@ -142,7 +142,7 @@ export const useEditor = (props: CustomEditorProps) => { editor?.commands.setContent(content, false, { preserveWhitespace: "full" }); }, setEditorValueAtCursorPosition: (content: string) => { - if (editor.state.selection) { + if (editor?.state.selection) { insertContentAtSavedSelection(editor, content); } }, diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index 12d4960b690..a0fa9f90c38 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -99,14 +99,11 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { if (!editor) return; scrollSummary(editor, marking); }, - getDocumentInfo: () => { - if (!editor) return; - return { - characters: editor.storage?.characterCount?.characters?.() ?? 0, - paragraphs: getParagraphCount(editor.state), - words: editor.storage?.characterCount?.words?.() ?? 0, - }; - }, + getDocumentInfo: () => ({ + characters: editor.storage?.characterCount?.characters?.() ?? 0, + paragraphs: getParagraphCount(editor.state), + words: editor.storage?.characterCount?.words?.() ?? 0, + }), })); if (!editor) { diff --git a/packages/editor/tsconfig.json b/packages/editor/tsconfig.json index 8edd9106fc2..58b8640e694 100644 --- a/packages/editor/tsconfig.json +++ b/packages/editor/tsconfig.json @@ -12,6 +12,7 @@ "@/styles/*": ["src/styles/*"], "@/plane-editor/*": ["src/ce/*"] }, + "strictNullChecks": true, "allowSyntheticDefaultImports": true }, "include": ["src/**/*", "index.d.ts"],