fix: rendering node views reliably

This commit is contained in:
Palanikannan M
2025-12-01 16:15:33 +05:30
parent 8db95d9ec0
commit 202336dd9c
16 changed files with 61 additions and 28 deletions

View File

@@ -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,

View File

@@ -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];

View File

@@ -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
@@ -33,6 +33,7 @@ export function CustomCalloutBlock(props: CustomCalloutNodeViewProps) {
style={{
backgroundColor: activeBackgroundColor,
}}
key={`callout-block-${node.attrs.id}`}
>
<CalloutBlockLogoSelector
blockAttributes={node.attrs}

View File

@@ -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",
@@ -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";

View File

@@ -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";
@@ -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>) => {
@@ -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"

View File

@@ -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() {

View File

@@ -0,0 +1,5 @@
import type { TBlockNodeBaseAttributes } from "../unique-id/types";
export type TCodeBlockAttributes = TBlockNodeBaseAttributes & {
language: string | null;
};

View File

@@ -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

View File

@@ -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",
@@ -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;

View File

@@ -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";
// extension config
import type { TMentionExtensionOptions } from "./extension-config";
// extension types
@@ -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",

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;
}

View File

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

View File

@@ -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;
};

View File

@@ -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;
};