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
1 change: 0 additions & 1 deletion apps/web/core/hooks/editor/use-editor-mention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export const useEditorMention = (args: TArgs) => {
name={user.member__display_name}
/>
),
id: user.member__id,
entity_identifier: user.member__id,
entity_name: "user_mention",
title: user.member__display_name,
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/core/constants/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ export const BLOCK_NODE_TYPES = [
CORE_EXTENSIONS.CALLOUT,
CORE_EXTENSIONS.WORK_ITEM_EMBED,
];

export const INLINE_NODE_TYPES = [CORE_EXTENSIONS.MENTION];
3 changes: 2 additions & 1 deletion packages/editor/src/core/extensions/callout/block.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NodeViewProps } from "@tiptap/react";
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
import React, { useState } from "react";
import { useState } from "react";
// constants
import { COLORS_LIST } from "@/constants/common";
// local components
Expand Down Expand Up @@ -33,6 +33,7 @@ export function CustomCalloutBlock(props: CustomCalloutNodeViewProps) {
style={{
backgroundColor: activeBackgroundColor,
}}
key={`callout-block-${node.attrs.id}`}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for TCalloutBlockAttributes type definition
echo "=== TCalloutBlockAttributes type definition ==="
rg -n "type TCalloutBlockAttributes|interface TCalloutBlockAttributes" --type=ts -A 10

# Search for TBlockNodeBaseAttributes to understand base type
echo -e "\n=== TBlockNodeBaseAttributes base type ==="
rg -n "type TBlockNodeBaseAttributes|interface TBlockNodeBaseAttributes" --type=ts -A 10

# Check callout block.tsx file to see what types are imported/used
echo -e "\n=== Callout block.tsx imports and type usage ==="
head -30 packages/editor/src/core/extensions/callout/block.tsx

# Find where callout nodes are created
echo -e "\n=== Callout node creation/initialization ==="
rg -n "createCalloutBlock|new.*Callout|attrs.*id" --type=ts -g '**/callout/**' -B 2 -A 2

Repository: makeplane/plane

Length of output: 5228


Key prop uses optional id attribute that could be undefined—ensure initialization guarantees unique IDs.

The key={callout-block-${node.attrs.id}} at line 36 depends on id from TBlockNodeBaseAttributes, which defines id as optional (id?: string | null). If id is undefined or null, the key becomes callout-block-undefined or callout-block-null, defeating the reconciliation objective of the PR.

Verify that callout nodes are always initialized with a valid, unique ID before rendering, or add runtime checks/defaults in the key expression to ensure stability.

🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/callout/block.tsx around line 36, the key
uses node.attrs.id which is optional and can be undefined/null; ensure callout
nodes always have a stable unique id by initializing a non-null id when nodes
are created (e.g., set a UUID in the node schema/creation step or when
deserializing), and also make the render-side key robust by using a runtime
fallback (use the existing id if present, otherwise a deterministic/generated
fallback stored on the node or a stable value tied to the node instance) so the
key never becomes "undefined" or "null".

>
<CalloutBlockLogoSelector
blockAttributes={node.attrs}
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/core/extensions/callout/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Node as ProseMirrorNode } from "@tiptap/core";
import type { TBlockNodeBaseAttributes } from "../unique-id/types";

export enum ECalloutAttributeNames {
ICON_COLOR = "data-icon-color",
Expand All @@ -20,7 +21,7 @@ export type TCalloutBlockEmojiAttributes = {
[ECalloutAttributeNames.EMOJI_URL]: string | undefined;
};

export type TCalloutBlockAttributes = {
export type TCalloutBlockAttributes = TBlockNodeBaseAttributes & {
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
[ECalloutAttributeNames.BACKGROUND]: string | undefined;
[ECalloutAttributeNames.BLOCK_TYPE]: "callout-component";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
import type { NodeViewProps } from "@tiptap/react";
import { NodeViewWrapper, NodeViewContent } from "@tiptap/react";
import ts from "highlight.js/lib/languages/typescript";
import { common, createLowlight } from "lowlight";
Expand All @@ -8,16 +8,20 @@ import { useState } from "react";
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// types
import type { TCodeBlockAttributes } from "./types";

// we just have ts support for now
const lowlight = createLowlight(common);
lowlight.register("ts", ts);

type Props = {
node: ProseMirrorNode;
export type CodeBlockNodeViewProps = NodeViewProps & {
node: NodeViewProps["node"] & {
attrs: TCodeBlockAttributes;
};
};

export function CodeBlockComponent({ node }: Props) {
export function CodeBlockComponent({ node }: CodeBlockNodeViewProps) {
const [copied, setCopied] = useState(false);

const copyToClipboard = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
Expand All @@ -33,7 +37,7 @@ export function CodeBlockComponent({ node }: Props) {
};

return (
<NodeViewWrapper className="code-block relative group/code">
<NodeViewWrapper className="code-block relative group/code" key={`code-block-${node.attrs.id}`}>
<Tooltip tooltipContent="Copy code">
<button
type="button"
Expand Down
5 changes: 4 additions & 1 deletion packages/editor/src/core/extensions/code/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { common, createLowlight } from "lowlight";
// components
import { CodeBlockLowlight } from "./code-block-lowlight";
import { CodeBlockComponent } from "./code-block-node-view";
import type { CodeBlockNodeViewProps } from "./code-block-node-view";

const lowlight = createLowlight(common);
lowlight.register("ts", ts);

export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
addNodeView() {
return ReactNodeViewRenderer(CodeBlockComponent);
return ReactNodeViewRenderer((props) => (
<CodeBlockComponent {...props} node={props.node as CodeBlockNodeViewProps["node"]} />
));
},

addKeyboardShortcuts() {
Expand Down
5 changes: 5 additions & 0 deletions packages/editor/src/core/extensions/code/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { TBlockNodeBaseAttributes } from "../unique-id/types";

export type TCodeBlockAttributes = TBlockNodeBaseAttributes & {
language: string | null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
const shouldShowBlock = (isUploaded || imageFromFileSystem) && !failedToLoadImage;

return (
<NodeViewWrapper>
<NodeViewWrapper key={`image-block-${node.attrs.id}`}>
<div className="p-0 mx-0 my-2" data-drag-handle ref={imageComponentRef}>
{shouldShowBlock && !hasDuplicationFailed ? (
<CustomImageBlock
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/core/extensions/custom-image/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Node } from "@tiptap/core";
// types
import type { TFileHandler } from "@/types";
import type { TBlockNodeBaseAttributes } from "../unique-id/types";

export enum ECustomImageAttributeNames {
ID = "id",
Expand Down Expand Up @@ -32,8 +33,7 @@ export enum ECustomImageStatus {
DUPLICATION_FAILED = "duplication-failed",
}

export type TCustomImageAttributes = {
[ECustomImageAttributeNames.ID]: string | null;
export type TCustomImageAttributes = TBlockNodeBaseAttributes & {
[ECustomImageAttributeNames.WIDTH]: PixelAttribute<"35%" | number> | null;
[ECustomImageAttributeNames.HEIGHT]: PixelAttribute<"auto" | number> | null;
[ECustomImageAttributeNames.ASPECT_RATIO]: number | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { NodeViewProps } from "@tiptap/react";
import { NodeViewWrapper } from "@tiptap/react";
import { useMemo } from "react";
import { v4 as uuidv4 } from "uuid";
Comment on lines +3 to +4
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused imports.

The imports useMemo and uuidv4 are not used anywhere in the file. The key on Line 24 directly uses attrs.id without generating a new UUID.

Apply this diff:

-import { useMemo } from "react";
-import { v4 as uuidv4 } from "uuid";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useMemo } from "react";
import { v4 as uuidv4 } from "uuid";
🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/mentions/mention-node-view.tsx around
lines 3–4, the imports `useMemo` and `v4 as uuidv4` are unused; remove them from
the import statement (or delete the entire import line if nothing else is used
from it) and ensure no leftover references to `uuidv4` exist (the component
already uses attrs.id directly on line 24), then run a quick compile/lint to
confirm no unused-import warnings remain.

// extension config
import type { TMentionExtensionOptions } from "./extension-config";
// extension types
Expand All @@ -19,7 +21,7 @@ export function MentionNodeView(props: MentionNodeViewProps) {
} = props;

return (
<NodeViewWrapper className="mention-component inline w-fit">
<NodeViewWrapper className="mention-component inline w-fit" key={`mention-${attrs.id}`}>
{(extension.options as TMentionExtensionOptions).renderComponent({
entity_identifier: attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER] ?? "",
entity_name: attrs[EMentionComponentAttributeNames.ENTITY_NAME] ?? "user_mention",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ export const MentionsListDropdown = forwardRef(function MentionsListDropdown(pro
(sectionIndex: number, itemIndex: number) => {
try {
const item = sections?.[sectionIndex]?.items?.[itemIndex];
const transactionId = uuidv4();
if (item) {
command({
...item,
id: transactionId,
});
}
} catch (error) {
Expand Down
5 changes: 2 additions & 3 deletions packages/editor/src/core/extensions/unique-id/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
import type { Transaction } from "@tiptap/pm/state";
import { v4 as uuidv4 } from "uuid";
// constants
import { CORE_EXTENSIONS, BLOCK_NODE_TYPES } from "@/constants/extension";
import { CORE_EXTENSIONS, BLOCK_NODE_TYPES, INLINE_NODE_TYPES } from "@/constants/extension";
import { ADDITIONAL_BLOCK_NODE_TYPES } from "@/plane-editor/constants/extensions";
import { createUniqueIDPlugin } from "./plugin";
import { createIdsForView } from "./utils";
// plane imports

const COMBINED_BLOCK_NODE_TYPES = [...BLOCK_NODE_TYPES, ...ADDITIONAL_BLOCK_NODE_TYPES];
const COMBINED_BLOCK_NODE_TYPES = [...INLINE_NODE_TYPES, ...BLOCK_NODE_TYPES, ...ADDITIONAL_BLOCK_NODE_TYPES];
export type UniqueIDGenerationContext = {
node: ProseMirrorNode;
pos: number;
Expand Down
7 changes: 7 additions & 0 deletions packages/editor/src/core/extensions/unique-id/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Base attributes for all block nodes that have the unique-id extension.
* All block node attribute types should extend this.
*/
export interface TBlockNodeBaseAttributes {
id?: string | null;
}
22 changes: 13 additions & 9 deletions packages/editor/src/core/extensions/work-item-embed/extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react";
import type { NodeViewProps } from "@tiptap/react";
// local imports
import { WorkItemEmbedExtensionConfig } from "./extension-config";
import type { TWorkItemEmbedAttributes } from "./types";

type Props = {
widgetCallback: ({
Expand All @@ -18,15 +19,18 @@ type Props = {
export function WorkItemEmbedExtension(props: Props) {
return WorkItemEmbedExtensionConfig.extend({
addNodeView() {
return ReactNodeViewRenderer((issueProps: NodeViewProps) => (
<NodeViewWrapper>
{props.widgetCallback({
issueId: issueProps.node.attrs.entity_identifier,
projectId: issueProps.node.attrs.project_identifier,
workspaceSlug: issueProps.node.attrs.workspace_identifier,
})}
</NodeViewWrapper>
));
return ReactNodeViewRenderer((issueProps: NodeViewProps) => {
const attrs = issueProps.node.attrs as TWorkItemEmbedAttributes;
return (
<NodeViewWrapper key={`work-item-embed-${attrs.id}`}>
{props.widgetCallback({
issueId: attrs.entity_identifier!,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Risky non-null assertion on optional field.

The non-null assertion attrs.entity_identifier! is unsafe since entity_identifier is defined as string | undefined in TWorkItemEmbedAttributes (Line 4 of work-item-embed/types.ts). If entity_identifier is undefined, this will pass undefined to widgetCallback, potentially causing runtime errors.

Consider adding a guard or early return:

       return ReactNodeViewRenderer((issueProps: NodeViewProps) => {
         const attrs = issueProps.node.attrs as TWorkItemEmbedAttributes;
+        if (!attrs.entity_identifier) {
+          return <NodeViewWrapper key={`work-item-embed-${attrs.id}`}>Invalid work item</NodeViewWrapper>;
+        }
         return (
           <NodeViewWrapper key={`work-item-embed-${attrs.id}`}>
             {props.widgetCallback({
-              issueId: attrs.entity_identifier!,
+              issueId: attrs.entity_identifier,
               projectId: attrs.project_identifier,
               workspaceSlug: attrs.workspace_identifier,
             })}
           </NodeViewWrapper>
         );
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
issueId: attrs.entity_identifier!,
return ReactNodeViewRenderer((issueProps: NodeViewProps) => {
const attrs = issueProps.node.attrs as TWorkItemEmbedAttributes;
if (!attrs.entity_identifier) {
return <NodeViewWrapper key={`work-item-embed-${attrs.id}`}>Invalid work item</NodeViewWrapper>;
}
return (
<NodeViewWrapper key={`work-item-embed-${attrs.id}`}>
{props.widgetCallback({
issueId: attrs.entity_identifier,
projectId: attrs.project_identifier,
workspaceSlug: attrs.workspace_identifier,
})}
</NodeViewWrapper>
);
});
🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/work-item-embed/extension.tsx around line
27, the code uses a risky non-null assertion for attrs.entity_identifier! which
is typed string | undefined; instead add a guard to check for undefined and
either return early or handle the missing id (e.g., log/warn and skip calling
widgetCallback, or provide a safe fallback) so widgetCallback never receives
undefined; update the surrounding code to validate attrs.entity_identifier
before building the payload and only call widgetCallback when a valid string is
present.

projectId: attrs.project_identifier,
workspaceSlug: attrs.workspace_identifier,
})}
</NodeViewWrapper>
);
});
},
});
}
8 changes: 8 additions & 0 deletions packages/editor/src/core/extensions/work-item-embed/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { TBlockNodeBaseAttributes } from "../unique-id/types";

export type TWorkItemEmbedAttributes = TBlockNodeBaseAttributes & {
entity_identifier: string | undefined;
project_identifier: string | undefined;
workspace_identifier: string | undefined;
entity_name: string | undefined;
};
2 changes: 1 addition & 1 deletion packages/editor/src/core/types/mention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type TMentionSuggestion = {
entity_identifier: string;
entity_name: TSearchEntities;
icon: React.ReactNode;
id: string;
id?: string | null;
subTitle?: string;
title: string;
};
Expand Down
Loading