Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
}
},
"scripts": {
"build": "tsc && tsup --minify",
"build": "tsc && tsup",
"dev": "tsup --watch",
"check:lint": "eslint . --max-warnings 30",
"check:types": "tsc --noEmit",
"check:types": "tsc",
"check:format": "prettier --check \"**/*.{ts,tsx,md,json,css,scss}\"",
"fix:lint": "eslint . --fix",
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\"",
Expand Down Expand Up @@ -87,6 +87,7 @@
"@types/node": "18.15.3",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^10.0.0",
"postcss": "^8.4.38",
"tsup": "8.4.0",
"typescript": "5.8.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const BubbleMenuNodeSelector: FC<Props> = (props) => {
TodoListItem(editor),
QuoteItem(editor),
CodeItem(editor),
];
] as EditorMenuItem<TEditorCommands>[];

const activeItem = items.filter((item) => item.isActive()).pop() ?? {
name: "Multiple",
Expand Down
49 changes: 30 additions & 19 deletions packages/editor/src/core/components/menus/bubble-menu/root.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection, useEditorState } from "@tiptap/react";
import { BubbleMenu, type BubbleMenuProps, type Editor, isNodeSelection, useEditorState } from "@tiptap/react";
import { FC, useEffect, useState, useRef } from "react";
// plane utils
import { cn } from "@plane/utils";
Expand Down Expand Up @@ -27,7 +27,9 @@ import { TEditorCommands } from "@/types";
// local imports
import { TextAlignmentSelector } from "./alignment-selector";

type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
editor: Editor;
};
Comment on lines +30 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align the required editor prop with the null-check

Prop type requires Editor, but you also guard for falsy editor at runtime. Choose one:

  • Make the prop nullable (Editor | null) and keep the guard, or
  • Keep Editor required and remove the guard.

Given useEditor returns Editor | null initially, prefer nullable prop.

-type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
-  editor: Editor;
-};
+type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
+  editor: Editor | null;
+};
@@
-  if (!editor) return null;
+  if (!editor) return null; // keep guard if prop can be null

If you keep Editor required, drop the guard instead.

Also applies to: 162-163

🤖 Prompt for AI Agents
In packages/editor/src/core/components/menus/bubble-menu/root.tsx around lines
30-32 (also apply to lines 162-163), the prop type declares editor as required
but the component guards against a falsy editor at runtime; change the prop type
to accept Editor | null and keep the runtime guard. Update the
EditorBubbleMenuProps declaration to use Editor | null, update any related prop
consumers/ts usages to pass nullable editors, and keep the existing
early-return/null-check logic in the component so TypeScript and runtime
behavior are consistent.


export type EditorStateType = {
code: boolean;
Expand All @@ -49,26 +51,29 @@ export type EditorStateType = {
| undefined;
};

export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Editor }) => {
const menuRef = useRef<HTMLDivElement>(null);
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
const { editor } = props;
// states
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
const [isSelecting, setIsSelecting] = useState(false);
// refs
const menuRef = useRef<HTMLDivElement>(null);

const formattingItems = {
code: CodeItem(props.editor),
bold: BoldItem(props.editor),
italic: ItalicItem(props.editor),
underline: UnderLineItem(props.editor),
strikethrough: StrikeThroughItem(props.editor),
"text-align": TextAlignItem(props.editor),
code: CodeItem(editor),
bold: BoldItem(editor),
italic: ItalicItem(editor),
underline: UnderLineItem(editor),
strikethrough: StrikeThroughItem(editor),
"text-align": TextAlignItem(editor),
} satisfies {
[K in TEditorCommands]?: EditorMenuItem<K>;
};

const editorState: EditorStateType = useEditorState({
editor: props.editor,
editor,
selector: ({ editor }: { editor: Editor }) => ({
code: formattingItems.code.isActive(),
bold: formattingItems.bold.isActive(),
Expand Down Expand Up @@ -111,10 +116,14 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
duration: [300, 0],
zIndex: 9,
onShow: () => {
props.editor.storage.link.isBubbleMenuOpen = true;
if (editor) {
editor.storage.link.isBubbleMenuOpen = true;
}
},
onHidden: () => {
props.editor.storage.link.isBubbleMenuOpen = false;
if (editor) {
editor.storage.link.isBubbleMenuOpen = false;
}
setIsNodeSelectorOpen(false);
setIsLinkSelectorOpen(false);
setIsColorSelectorOpen(false);
Expand All @@ -127,7 +136,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
if (menuRef.current?.contains(e.target as Node)) return;

function handleMouseMove() {
if (!props.editor.state.selection.empty) {
if (!editor?.state.selection.empty) {
setIsSelecting(true);
document.removeEventListener("mousemove", handleMouseMove);
}
Expand All @@ -148,7 +157,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
return () => {
document.removeEventListener("mousedown", handleMouseDown);
};
}, [props.editor]);
}, [editor]);

if (!editor) return null;

return (
<BubbleMenu {...bubbleMenuProps}>
Expand All @@ -159,7 +170,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
>
<div className="px-2">
<BubbleMenuNodeSelector
editor={props.editor!}
editor={editor}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen((prev) => !prev);
Expand All @@ -171,7 +182,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
{!editorState.code && (
<div className="px-2">
<BubbleMenuLinkSelector
editor={props.editor}
editor={editor}
isOpen={isLinkSelectorOpen}
setIsOpen={() => {
setIsLinkSelectorOpen((prev) => !prev);
Expand All @@ -184,7 +195,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
{!editorState.code && (
<div className="px-2">
<BubbleMenuColorSelector
editor={props.editor}
editor={editor}
isOpen={isColorSelectorOpen}
editorState={editorState}
setIsOpen={() => {
Expand Down Expand Up @@ -215,7 +226,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
</button>
))}
</div>
<TextAlignmentSelector editor={props.editor} editorState={editorState} />
<TextAlignmentSelector editor={editor} editorState={editorState} />
</div>
)}
</BubbleMenu>
Expand Down
8 changes: 4 additions & 4 deletions packages/editor/src/core/components/menus/menu-items.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Editor } from "@tiptap/react";
import type { Editor } from "@tiptap/react";
import {
BoldIcon,
Heading1,
Expand All @@ -18,7 +18,7 @@ import {
Heading5,
Heading6,
CaseSensitive,
LucideIcon,
type LucideIcon,
MinusSquare,
Palette,
AlignCenter,
Expand Down Expand Up @@ -70,7 +70,7 @@ export const TextItem = (editor: Editor): EditorMenuItem<"text"> => ({
icon: CaseSensitive,
});

type SupportedHeadingLevels = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
type SupportedHeadingLevels = Extract<TEditorCommands, "h1" | "h2" | "h3" | "h4" | "h5" | "h6">;

const HeadingItem = <T extends SupportedHeadingLevels>(
editor: Editor,
Expand Down Expand Up @@ -274,5 +274,5 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem<TEdito
TextColorItem(editor),
BackgroundColorItem(editor),
TextAlignItem(editor),
];
] as EditorMenuItem<TEditorCommands>[];
};
4 changes: 2 additions & 2 deletions packages/editor/src/core/extensions/callout/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { COLORS_LIST } from "@/constants/common";
import { CalloutBlockColorSelector } from "./color-selector";
import { CalloutBlockLogoSelector } from "./logo-selector";
// types
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
import { ECalloutAttributeNames, TCalloutBlockAttributes } from "./types";
// utils
import { updateStoredBackgroundColor } from "./utils";

Expand Down Expand Up @@ -45,7 +45,7 @@ export const CustomCalloutBlock: React.FC<CustomCalloutNodeViewProps> = (props)
toggleDropdown={() => setIsColorPickerOpen((prev) => !prev)}
onSelect={(val) => {
updateAttributes({
[EAttributeNames.BACKGROUND]: val,
[ECalloutAttributeNames.BACKGROUND]: val,
});
updateStoredBackgroundColor(val);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Node as NodeType } from "@tiptap/pm/model";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// types
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
import { ECalloutAttributeNames, TCalloutBlockAttributes } from "./types";
// utils
import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils";

Expand All @@ -25,7 +25,7 @@ export const CustomCalloutExtensionConfig = Node.create({
addAttributes() {
const attributes = {
// Reduce instead of map to accumulate the attributes directly into an object
...Object.values(EAttributeNames).reduce((acc, value) => {
...Object.values(ECalloutAttributeNames).reduce((acc, value) => {
acc[value] = {
default: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[value],
};
Expand Down Expand Up @@ -62,7 +62,7 @@ export const CustomCalloutExtensionConfig = Node.create({
parseHTML() {
return [
{
tag: `div[${EAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[EAttributeNames.BLOCK_TYPE]}"]`,
tag: `div[${ECalloutAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.BLOCK_TYPE]}"]`,
},
];
},
Expand Down
16 changes: 8 additions & 8 deletions packages/editor/src/core/extensions/callout/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum EAttributeNames {
export enum ECalloutAttributeNames {
ICON_COLOR = "data-icon-color",
ICON_NAME = "data-icon-name",
EMOJI_UNICODE = "data-emoji-unicode",
Expand All @@ -9,18 +9,18 @@ export enum EAttributeNames {
}

export type TCalloutBlockIconAttributes = {
[EAttributeNames.ICON_COLOR]: string | undefined;
[EAttributeNames.ICON_NAME]: string | undefined;
[ECalloutAttributeNames.ICON_COLOR]: string | undefined;
[ECalloutAttributeNames.ICON_NAME]: string | undefined;
};

export type TCalloutBlockEmojiAttributes = {
[EAttributeNames.EMOJI_UNICODE]: string | undefined;
[EAttributeNames.EMOJI_URL]: string | undefined;
[ECalloutAttributeNames.EMOJI_UNICODE]: string | undefined;
[ECalloutAttributeNames.EMOJI_URL]: string | undefined;
};

export type TCalloutBlockAttributes = {
[EAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
[EAttributeNames.BACKGROUND]: string | undefined;
[EAttributeNames.BLOCK_TYPE]: "callout-component";
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
[ECalloutAttributeNames.BACKGROUND]: string | undefined;
[ECalloutAttributeNames.BLOCK_TYPE]: "callout-component";
} & TCalloutBlockIconAttributes &
TCalloutBlockEmojiAttributes;
42 changes: 23 additions & 19 deletions packages/editor/src/core/extensions/callout/utils.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
// plane imports
import { TEmojiLogoProps } from "@plane/ui";
import type { TEmojiLogoProps } from "@plane/ui";
import { sanitizeHTML } from "@plane/utils";
// types
import {
EAttributeNames,
ECalloutAttributeNames,
TCalloutBlockAttributes,
TCalloutBlockEmojiAttributes,
TCalloutBlockIconAttributes,
} from "./types";

export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = {
"data-logo-in-use": "emoji",
"data-icon-color": undefined,
"data-icon-name": undefined,
"data-emoji-unicode": "128161",
"data-emoji-url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png",
"data-background": undefined,
"data-block-type": "callout-component",
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji",
[ECalloutAttributeNames.ICON_COLOR]: undefined,
[ECalloutAttributeNames.ICON_NAME]: undefined,
[ECalloutAttributeNames.EMOJI_UNICODE]: "128161",
[ECalloutAttributeNames.EMOJI_URL]: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png",
[ECalloutAttributeNames.BACKGROUND]: undefined,
[ECalloutAttributeNames.BLOCK_TYPE]: "callout-component",
};

type TStoredLogoValue = Pick<TCalloutBlockAttributes, EAttributeNames.LOGO_IN_USE> &
type TStoredLogoValue = Pick<TCalloutBlockAttributes, ECalloutAttributeNames.LOGO_IN_USE> &
(TCalloutBlockEmojiAttributes | TCalloutBlockIconAttributes);

// function to get the stored logo from local storage
export const getStoredLogo = (): TStoredLogoValue => {
const fallBackValues: TStoredLogoValue = {
"data-logo-in-use": "emoji",
"data-emoji-unicode": DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
"data-emoji-url": DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"],
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji",
[ECalloutAttributeNames.EMOJI_UNICODE]: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.EMOJI_UNICODE],
[ECalloutAttributeNames.EMOJI_URL]: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.EMOJI_URL],
};

if (typeof window !== "undefined") {
Expand All @@ -43,16 +43,20 @@ export const getStoredLogo = (): TStoredLogoValue => {
}
if (parsedData.in_use === "emoji" && parsedData.emoji?.value) {
return {
"data-logo-in-use": "emoji",
"data-emoji-unicode": parsedData.emoji.value || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
"data-emoji-url": parsedData.emoji.url || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"],
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji",
[ECalloutAttributeNames.EMOJI_UNICODE]:
parsedData.emoji.value || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.EMOJI_UNICODE],
[ECalloutAttributeNames.EMOJI_URL]:
parsedData.emoji.url || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.EMOJI_URL],
};
}
if (parsedData.in_use === "icon" && parsedData.icon?.name) {
return {
"data-logo-in-use": "icon",
"data-icon-name": parsedData.icon.name || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-icon-name"],
"data-icon-color": parsedData.icon.color || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-icon-color"],
[ECalloutAttributeNames.LOGO_IN_USE]: "icon",
[ECalloutAttributeNames.ICON_NAME]:
parsedData.icon.name || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.ICON_NAME],
[ECalloutAttributeNames.ICON_COLOR]:
parsedData.icon.color || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.ICON_COLOR],
};
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/editor/src/core/extensions/code/code-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
default: null,
parseHTML: (element) => {
const { languageClassPrefix } = this.options;
// @ts-expect-error element is a DOM element
const classNames = [...(element.firstElementChild?.classList || [])];
const languages = classNames
.filter((className) => className.startsWith(languageClassPrefix))
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/core/extensions/emoji/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { emojiSuggestion } from "./suggestion";

export const EmojiExtension = Emoji.extend({
addStorage() {
const extensionOptions = this.options;
return {
...this.parent?.(),
markdown: {
serialize(state: MarkdownSerializerState, node: ProseMirrorNode) {
const emojiItem = shortcodeToEmoji(node.attrs.name, this.options.emojis);
const emojiItem = shortcodeToEmoji(node.attrs.name, extensionOptions.emojis);
if (emojiItem?.emoji) {
state.write(emojiItem?.emoji);
} else if (emojiItem?.fallbackImage) {
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/core/extensions/emoji/suggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const emojiSuggestion: EmojiOptions["suggestion"] = {

if (query.trim() === "") {
const defaultEmojis = DEFAULT_EMOJIS.map((name) =>
filteredEmojis.find((emoji: EmojiItem) => emoji.shortcodes.includes(name) || emoji.name === name)
filteredEmojis.find((emoji) => emoji.shortcodes.includes(name) || emoji.name === name)
)
.filter(Boolean)
.slice(0, 5);
Expand Down
Loading