mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 20:07:56 +01:00
fix: rendering node views reliably
This commit is contained in:
@@ -46,7 +46,6 @@ export const useEditorMention = (args: TArgs) => {
|
|||||||
name={user.member__display_name}
|
name={user.member__display_name}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
id: user.member__id,
|
|
||||||
entity_identifier: user.member__id,
|
entity_identifier: user.member__id,
|
||||||
entity_name: "user_mention",
|
entity_name: "user_mention",
|
||||||
title: user.member__display_name,
|
title: user.member__display_name,
|
||||||
|
|||||||
@@ -69,3 +69,5 @@ export const BLOCK_NODE_TYPES = [
|
|||||||
CORE_EXTENSIONS.CALLOUT,
|
CORE_EXTENSIONS.CALLOUT,
|
||||||
CORE_EXTENSIONS.WORK_ITEM_EMBED,
|
CORE_EXTENSIONS.WORK_ITEM_EMBED,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const INLINE_NODE_TYPES = [CORE_EXTENSIONS.MENTION];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NodeViewProps } from "@tiptap/react";
|
import type { NodeViewProps } from "@tiptap/react";
|
||||||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
// constants
|
// constants
|
||||||
import { COLORS_LIST } from "@/constants/common";
|
import { COLORS_LIST } from "@/constants/common";
|
||||||
// local components
|
// local components
|
||||||
@@ -33,6 +33,7 @@ export function CustomCalloutBlock(props: CustomCalloutNodeViewProps) {
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: activeBackgroundColor,
|
backgroundColor: activeBackgroundColor,
|
||||||
}}
|
}}
|
||||||
|
key={`callout-block-${node.attrs.id}`}
|
||||||
>
|
>
|
||||||
<CalloutBlockLogoSelector
|
<CalloutBlockLogoSelector
|
||||||
blockAttributes={node.attrs}
|
blockAttributes={node.attrs}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Node as ProseMirrorNode } from "@tiptap/core";
|
import type { Node as ProseMirrorNode } from "@tiptap/core";
|
||||||
|
import type { TBlockNodeBaseAttributes } from "../unique-id/types";
|
||||||
|
|
||||||
export enum ECalloutAttributeNames {
|
export enum ECalloutAttributeNames {
|
||||||
ICON_COLOR = "data-icon-color",
|
ICON_COLOR = "data-icon-color",
|
||||||
@@ -20,7 +21,7 @@ export type TCalloutBlockEmojiAttributes = {
|
|||||||
[ECalloutAttributeNames.EMOJI_URL]: string | undefined;
|
[ECalloutAttributeNames.EMOJI_URL]: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCalloutBlockAttributes = {
|
export type TCalloutBlockAttributes = TBlockNodeBaseAttributes & {
|
||||||
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
|
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
|
||||||
[ECalloutAttributeNames.BACKGROUND]: string | undefined;
|
[ECalloutAttributeNames.BACKGROUND]: string | undefined;
|
||||||
[ECalloutAttributeNames.BLOCK_TYPE]: "callout-component";
|
[ECalloutAttributeNames.BLOCK_TYPE]: "callout-component";
|
||||||
|
|||||||
@@ -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 { NodeViewWrapper, NodeViewContent } from "@tiptap/react";
|
||||||
import ts from "highlight.js/lib/languages/typescript";
|
import ts from "highlight.js/lib/languages/typescript";
|
||||||
import { common, createLowlight } from "lowlight";
|
import { common, createLowlight } from "lowlight";
|
||||||
@@ -8,16 +8,20 @@ import { useState } from "react";
|
|||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// plane utils
|
// plane utils
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
// types
|
||||||
|
import type { TCodeBlockAttributes } from "./types";
|
||||||
|
|
||||||
// we just have ts support for now
|
// we just have ts support for now
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("ts", ts);
|
lowlight.register("ts", ts);
|
||||||
|
|
||||||
type Props = {
|
export type CodeBlockNodeViewProps = NodeViewProps & {
|
||||||
node: ProseMirrorNode;
|
node: NodeViewProps["node"] & {
|
||||||
|
attrs: TCodeBlockAttributes;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CodeBlockComponent({ node }: Props) {
|
export function CodeBlockComponent({ node }: CodeBlockNodeViewProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const copyToClipboard = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
const copyToClipboard = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
@@ -33,7 +37,7 @@ export function CodeBlockComponent({ node }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<Tooltip tooltipContent="Copy code">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import { common, createLowlight } from "lowlight";
|
|||||||
// components
|
// components
|
||||||
import { CodeBlockLowlight } from "./code-block-lowlight";
|
import { CodeBlockLowlight } from "./code-block-lowlight";
|
||||||
import { CodeBlockComponent } from "./code-block-node-view";
|
import { CodeBlockComponent } from "./code-block-node-view";
|
||||||
|
import type { CodeBlockNodeViewProps } from "./code-block-node-view";
|
||||||
|
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("ts", ts);
|
lowlight.register("ts", ts);
|
||||||
|
|
||||||
export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
|
export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(CodeBlockComponent);
|
return ReactNodeViewRenderer((props) => (
|
||||||
|
<CodeBlockComponent {...props} node={props.node as CodeBlockNodeViewProps["node"]} />
|
||||||
|
));
|
||||||
},
|
},
|
||||||
|
|
||||||
addKeyboardShortcuts() {
|
addKeyboardShortcuts() {
|
||||||
|
|||||||
5
packages/editor/src/core/extensions/code/types.ts
Normal file
5
packages/editor/src/core/extensions/code/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { TBlockNodeBaseAttributes } from "../unique-id/types";
|
||||||
|
|
||||||
|
export type TCodeBlockAttributes = TBlockNodeBaseAttributes & {
|
||||||
|
language: string | null;
|
||||||
|
};
|
||||||
@@ -122,7 +122,7 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
|
|||||||
const shouldShowBlock = (isUploaded || imageFromFileSystem) && !failedToLoadImage;
|
const shouldShowBlock = (isUploaded || imageFromFileSystem) && !failedToLoadImage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper>
|
<NodeViewWrapper key={`image-block-${node.attrs.id}`}>
|
||||||
<div className="p-0 mx-0 my-2" data-drag-handle ref={imageComponentRef}>
|
<div className="p-0 mx-0 my-2" data-drag-handle ref={imageComponentRef}>
|
||||||
{shouldShowBlock && !hasDuplicationFailed ? (
|
{shouldShowBlock && !hasDuplicationFailed ? (
|
||||||
<CustomImageBlock
|
<CustomImageBlock
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Node } from "@tiptap/core";
|
import type { Node } from "@tiptap/core";
|
||||||
// types
|
// types
|
||||||
import type { TFileHandler } from "@/types";
|
import type { TFileHandler } from "@/types";
|
||||||
|
import type { TBlockNodeBaseAttributes } from "../unique-id/types";
|
||||||
|
|
||||||
export enum ECustomImageAttributeNames {
|
export enum ECustomImageAttributeNames {
|
||||||
ID = "id",
|
ID = "id",
|
||||||
@@ -32,8 +33,7 @@ export enum ECustomImageStatus {
|
|||||||
DUPLICATION_FAILED = "duplication-failed",
|
DUPLICATION_FAILED = "duplication-failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCustomImageAttributes = {
|
export type TCustomImageAttributes = TBlockNodeBaseAttributes & {
|
||||||
[ECustomImageAttributeNames.ID]: string | null;
|
|
||||||
[ECustomImageAttributeNames.WIDTH]: PixelAttribute<"35%" | number> | null;
|
[ECustomImageAttributeNames.WIDTH]: PixelAttribute<"35%" | number> | null;
|
||||||
[ECustomImageAttributeNames.HEIGHT]: PixelAttribute<"auto" | number> | null;
|
[ECustomImageAttributeNames.HEIGHT]: PixelAttribute<"auto" | number> | null;
|
||||||
[ECustomImageAttributeNames.ASPECT_RATIO]: number | null;
|
[ECustomImageAttributeNames.ASPECT_RATIO]: number | null;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { NodeViewProps } from "@tiptap/react";
|
import type { NodeViewProps } from "@tiptap/react";
|
||||||
import { NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewWrapper } from "@tiptap/react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
// extension config
|
// extension config
|
||||||
import type { TMentionExtensionOptions } from "./extension-config";
|
import type { TMentionExtensionOptions } from "./extension-config";
|
||||||
// extension types
|
// extension types
|
||||||
@@ -19,7 +21,7 @@ export function MentionNodeView(props: MentionNodeViewProps) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className="mention-component inline w-fit">
|
<NodeViewWrapper className="mention-component inline w-fit" key={`mention-${attrs.id}`}>
|
||||||
{(extension.options as TMentionExtensionOptions).renderComponent({
|
{(extension.options as TMentionExtensionOptions).renderComponent({
|
||||||
entity_identifier: attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER] ?? "",
|
entity_identifier: attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER] ?? "",
|
||||||
entity_name: attrs[EMentionComponentAttributeNames.ENTITY_NAME] ?? "user_mention",
|
entity_name: attrs[EMentionComponentAttributeNames.ENTITY_NAME] ?? "user_mention",
|
||||||
|
|||||||
@@ -31,11 +31,9 @@ export const MentionsListDropdown = forwardRef(function MentionsListDropdown(pro
|
|||||||
(sectionIndex: number, itemIndex: number) => {
|
(sectionIndex: number, itemIndex: number) => {
|
||||||
try {
|
try {
|
||||||
const item = sections?.[sectionIndex]?.items?.[itemIndex];
|
const item = sections?.[sectionIndex]?.items?.[itemIndex];
|
||||||
const transactionId = uuidv4();
|
|
||||||
if (item) {
|
if (item) {
|
||||||
command({
|
command({
|
||||||
...item,
|
...item,
|
||||||
id: transactionId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
|||||||
import type { Transaction } from "@tiptap/pm/state";
|
import type { Transaction } from "@tiptap/pm/state";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
// constants
|
// 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 { ADDITIONAL_BLOCK_NODE_TYPES } from "@/plane-editor/constants/extensions";
|
||||||
import { createUniqueIDPlugin } from "./plugin";
|
import { createUniqueIDPlugin } from "./plugin";
|
||||||
import { createIdsForView } from "./utils";
|
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 = {
|
export type UniqueIDGenerationContext = {
|
||||||
node: ProseMirrorNode;
|
node: ProseMirrorNode;
|
||||||
pos: number;
|
pos: number;
|
||||||
|
|||||||
7
packages/editor/src/core/extensions/unique-id/types.ts
Normal file
7
packages/editor/src/core/extensions/unique-id/types.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react";
|
|||||||
import type { NodeViewProps } from "@tiptap/react";
|
import type { NodeViewProps } from "@tiptap/react";
|
||||||
// local imports
|
// local imports
|
||||||
import { WorkItemEmbedExtensionConfig } from "./extension-config";
|
import { WorkItemEmbedExtensionConfig } from "./extension-config";
|
||||||
|
import type { TWorkItemEmbedAttributes } from "./types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
widgetCallback: ({
|
widgetCallback: ({
|
||||||
@@ -18,15 +19,18 @@ type Props = {
|
|||||||
export function WorkItemEmbedExtension(props: Props) {
|
export function WorkItemEmbedExtension(props: Props) {
|
||||||
return WorkItemEmbedExtensionConfig.extend({
|
return WorkItemEmbedExtensionConfig.extend({
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer((issueProps: NodeViewProps) => (
|
return ReactNodeViewRenderer((issueProps: NodeViewProps) => {
|
||||||
<NodeViewWrapper>
|
const attrs = issueProps.node.attrs as TWorkItemEmbedAttributes;
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper key={`work-item-embed-${attrs.id}`}>
|
||||||
{props.widgetCallback({
|
{props.widgetCallback({
|
||||||
issueId: issueProps.node.attrs.entity_identifier,
|
issueId: attrs.entity_identifier!,
|
||||||
projectId: issueProps.node.attrs.project_identifier,
|
projectId: attrs.project_identifier,
|
||||||
workspaceSlug: issueProps.node.attrs.workspace_identifier,
|
workspaceSlug: attrs.workspace_identifier,
|
||||||
})}
|
})}
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
));
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -5,7 +5,7 @@ export type TMentionSuggestion = {
|
|||||||
entity_identifier: string;
|
entity_identifier: string;
|
||||||
entity_name: TSearchEntities;
|
entity_name: TSearchEntities;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
id: string;
|
id?: string | null;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user