mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
fix: node view renders (#8559)
* fix node renders * fix handlers * fix: duplicate id
This commit is contained in:
@@ -29,13 +29,14 @@ export function CustomCalloutBlock(props: CustomCalloutNodeViewProps) {
|
||||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
key={node.attrs[ECalloutAttributeNames.ID]}
|
||||
className="editor-callout-component group/callout-node relative bg-layer-3 rounded-lg text-primary p-4 my-2 flex items-start gap-4 transition-colors duration-500 break-words"
|
||||
style={{
|
||||
backgroundColor: activeBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<CalloutBlockLogoSelector
|
||||
key={node.attrs["id"]}
|
||||
key={node.attrs[ECalloutAttributeNames.ID]}
|
||||
blockAttributes={node.attrs}
|
||||
disabled={!editor.isEditable}
|
||||
isOpen={isEmojiPickerOpen}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node as ProseMirrorNode } from "@tiptap/core";
|
||||
|
||||
export enum ECalloutAttributeNames {
|
||||
ID = "id",
|
||||
ICON_COLOR = "data-icon-color",
|
||||
ICON_NAME = "data-icon-name",
|
||||
EMOJI_UNICODE = "data-emoji-unicode",
|
||||
@@ -21,6 +22,7 @@ export type TCalloutBlockEmojiAttributes = {
|
||||
};
|
||||
|
||||
export type TCalloutBlockAttributes = {
|
||||
[ECalloutAttributeNames.ID]: string | null;
|
||||
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
|
||||
[ECalloutAttributeNames.BACKGROUND]: string | undefined;
|
||||
[ECalloutAttributeNames.BLOCK_TYPE]: "callout-component";
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { TCalloutBlockAttributes, TCalloutBlockEmojiAttributes, TCalloutBlo
|
||||
import { ECalloutAttributeNames } from "./types";
|
||||
|
||||
export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = {
|
||||
[ECalloutAttributeNames.ID]: null,
|
||||
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji",
|
||||
[ECalloutAttributeNames.ICON_COLOR]: undefined,
|
||||
[ECalloutAttributeNames.ICON_NAME]: undefined,
|
||||
@@ -31,7 +32,7 @@ export const getStoredLogo = (): TStoredLogoValue => {
|
||||
if (storedData) {
|
||||
let parsedData: TLogoProps;
|
||||
try {
|
||||
parsedData = JSON.parse(storedData);
|
||||
parsedData = JSON.parse(storedData) as TLogoProps;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing stored callout logo, stored value- ${storedData}`, error);
|
||||
localStorage.removeItem("editor-calloutComponent-logo");
|
||||
|
||||
@@ -9,6 +9,9 @@ import { CopyIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// types
|
||||
import type { TCodeBlockAttributes } from "./types";
|
||||
import { ECodeBlockAttributeNames } from "./types";
|
||||
|
||||
// we just have ts support for now
|
||||
const lowlight = createLowlight(common);
|
||||
@@ -20,6 +23,8 @@ type Props = {
|
||||
|
||||
export function CodeBlockComponent({ node }: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
// derived values
|
||||
const attrs = node.attrs as TCodeBlockAttributes;
|
||||
|
||||
const copyToClipboard = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
try {
|
||||
@@ -34,7 +39,7 @@ export function CodeBlockComponent({ node }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="code-block relative group/code">
|
||||
<NodeViewWrapper key={attrs[ECodeBlockAttributeNames.ID]} className="code-block relative group/code">
|
||||
<Tooltip tooltipContent="Copy code">
|
||||
<button
|
||||
type="button"
|
||||
@@ -44,7 +49,7 @@ export function CodeBlockComponent({ node }: Props) {
|
||||
"bg-success-subtle hover:bg-success-subtle-1 active:bg-success-subtle-1": copied,
|
||||
}
|
||||
)}
|
||||
onClick={copyToClipboard}
|
||||
onClick={(e) => void copyToClipboard(e)}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="h-3 w-3 text-success-primary" strokeWidth={3} />
|
||||
|
||||
9
packages/editor/src/core/extensions/code/types.ts
Normal file
9
packages/editor/src/core/extensions/code/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum ECodeBlockAttributeNames {
|
||||
ID = "id",
|
||||
LANGUAGE = "language",
|
||||
}
|
||||
|
||||
export type TCodeBlockAttributes = {
|
||||
[ECodeBlockAttributeNames.ID]: string | null;
|
||||
[ECodeBlockAttributeNames.LANGUAGE]: string | null;
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// local imports
|
||||
import { ECustomImageAttributeNames } from "../types";
|
||||
import type { Pixel, TCustomImageAttributes, TCustomImageSize } from "../types";
|
||||
import { ensurePixelString, getImageBlockId, isImageDuplicating } from "../utils";
|
||||
import type { CustomImageNodeViewProps } from "./node-view";
|
||||
@@ -59,7 +60,7 @@ export function CustomImageBlock(props: CustomImageBlockProps) {
|
||||
const [hasErroredOnFirstLoad, setHasErroredOnFirstLoad] = useState(false);
|
||||
const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false);
|
||||
// extension options
|
||||
const isTouchDevice = !!editor.storage.utility.isTouchDevice;
|
||||
const isTouchDevice = !!(editor.storage.utility as { isTouchDevice?: boolean } | undefined)?.isTouchDevice;
|
||||
|
||||
const updateAttributesSafely = useCallback(
|
||||
(attributes: Partial<TCustomImageAttributes>, errorMessage: string) => {
|
||||
@@ -218,7 +219,11 @@ export function CustomImageBlock(props: CustomImageBlockProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
id={getImageBlockId(node.attrs.id ?? "")}
|
||||
id={
|
||||
node.attrs[ECustomImageAttributeNames.ID]
|
||||
? getImageBlockId(node.attrs[ECustomImageAttributeNames.ID])
|
||||
: undefined
|
||||
}
|
||||
className={cn("w-fit max-w-full transition-all", {
|
||||
"ml-[50%] -translate-x-1/2": nodeAlignment === "center",
|
||||
"ml-[100%] -translate-x-full": nodeAlignment === "right",
|
||||
@@ -239,42 +244,45 @@ export function CustomImageBlock(props: CustomImageBlockProps) {
|
||||
<img
|
||||
ref={imageRef}
|
||||
src={displayedImageSrc}
|
||||
alt=""
|
||||
onLoad={handleImageLoad}
|
||||
onError={async (e) => {
|
||||
// for old image extension this command doesn't exist or if the image failed to load for the first time
|
||||
if (!extension.options.restoreImage || hasTriedRestoringImageOnce) {
|
||||
setFailedToLoadImage(true);
|
||||
return;
|
||||
}
|
||||
onError={(e) =>
|
||||
void (async () => {
|
||||
// for old image extension this command doesn't exist or if the image failed to load for the first time
|
||||
if (!extension.options.restoreImage || hasTriedRestoringImageOnce) {
|
||||
setFailedToLoadImage(true);
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
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 extension.options.restoreImage?.(imgNodeSrc);
|
||||
if (!imageRef.current) {
|
||||
throw new Error("Image reference not found");
|
||||
}
|
||||
if (!resolvedImageSrc) {
|
||||
throw new Error("No resolved image source available");
|
||||
}
|
||||
if (isTouchDevice) {
|
||||
const refreshedSrc = await extension.options.getImageSource?.(imgNodeSrc);
|
||||
imageRef.current.src = refreshedSrc;
|
||||
} else {
|
||||
imageRef.current.src = resolvedImageSrc;
|
||||
}
|
||||
} catch (error) {
|
||||
// if the image failed to even restore, then show the error state
|
||||
setFailedToLoadImage(true);
|
||||
console.error("Error while loading image", error);
|
||||
} finally {
|
||||
setHasErroredOnFirstLoad(false);
|
||||
setHasTriedRestoringImageOnce(true);
|
||||
}
|
||||
await extension.options.restoreImage?.(imgNodeSrc);
|
||||
if (!imageRef.current) {
|
||||
throw new Error("Image reference not found");
|
||||
}
|
||||
if (!resolvedImageSrc) {
|
||||
throw new Error("No resolved image source available");
|
||||
}
|
||||
if (isTouchDevice) {
|
||||
const refreshedSrc = await extension.options.getImageSource?.(imgNodeSrc);
|
||||
imageRef.current.src = refreshedSrc;
|
||||
} else {
|
||||
imageRef.current.src = resolvedImageSrc;
|
||||
}
|
||||
} catch {
|
||||
// if the image failed to even restore, then show the error state
|
||||
setFailedToLoadImage(true);
|
||||
console.error("Error while loading image", e);
|
||||
} finally {
|
||||
setHasErroredOnFirstLoad(false);
|
||||
setHasTriedRestoringImageOnce(true);
|
||||
}
|
||||
}}
|
||||
})()
|
||||
}
|
||||
width={size.width}
|
||||
className={cn("image-component block rounded-md", {
|
||||
// hide the image while the background calculations of the image loader are in progress (to avoid flickering) and show the loader until then
|
||||
@@ -287,7 +295,9 @@ export function CustomImageBlock(props: CustomImageBlockProps) {
|
||||
...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
|
||||
}}
|
||||
/>
|
||||
{showUploadStatus && node.attrs.id && <ImageUploadStatus editor={editor} nodeId={node.attrs.id} />}
|
||||
{showUploadStatus && node.attrs[ECustomImageAttributeNames.ID] && (
|
||||
<ImageUploadStatus editor={editor} nodeId={node.attrs[ECustomImageAttributeNames.ID]} />
|
||||
)}
|
||||
{showImageToolbar && (
|
||||
<ImageToolbarRoot
|
||||
alignment={nodeAlignment ?? "left"}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { NodeViewProps } from "@tiptap/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
// local imports
|
||||
import type { CustomImageExtensionType, TCustomImageAttributes } from "../types";
|
||||
import { ECustomImageStatus } from "../types";
|
||||
import { ECustomImageAttributeNames, ECustomImageStatus } from "../types";
|
||||
import { hasImageDuplicationFailed } from "../utils";
|
||||
import { CustomImageBlock } from "./block";
|
||||
import { CustomImageUploader } from "./uploader";
|
||||
@@ -71,8 +71,9 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
|
||||
setFailedToLoadImage(true);
|
||||
}
|
||||
};
|
||||
getImageSource();
|
||||
}, [imgNodeSrc, extension.options]);
|
||||
void getImageSource();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [imgNodeSrc, extension.options.getImageSource, extension.options.getImageDownloadSource]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleDuplication = async () => {
|
||||
@@ -106,7 +107,8 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
|
||||
}
|
||||
};
|
||||
|
||||
handleDuplication();
|
||||
void handleDuplication();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [status, imgNodeSrc, extension.options.duplicateImage, updateAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -129,7 +131,7 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
|
||||
const shouldShowBlock = hasValidImageSource && !failedToLoadImage && !hasDuplicationFailed;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<NodeViewWrapper key={node.attrs[ECustomImageAttributeNames.ID]}>
|
||||
<div className="p-0 mx-0 my-2" data-drag-handle ref={imageComponentRef}>
|
||||
{shouldShowBlock && !hasDuplicationFailed ? (
|
||||
<CustomImageBlock
|
||||
@@ -146,7 +148,7 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
|
||||
failedToLoadImage={failedToLoadImage}
|
||||
hasDuplicationFailed={hasDuplicationFailed}
|
||||
loadImageFromFileSystem={setImageFromFileSystem}
|
||||
maxFileSize={editor.storage.imageComponent?.maxFileSize}
|
||||
maxFileSize={(editor.storage.imageComponent as { maxFileSize?: number } | undefined)?.maxFileSize ?? 0}
|
||||
setIsUploaded={setIsUploaded}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function MentionNodeView(props: MentionNodeViewProps) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="mention-component inline w-fit">
|
||||
<NodeViewWrapper key={attrs[EMentionComponentAttributeNames.ID]} className="mention-component inline w-fit">
|
||||
{(extension.options as TMentionExtensionOptions).renderComponent({
|
||||
entity_identifier: attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER] ?? "",
|
||||
entity_name: attrs[EMentionComponentAttributeNames.ENTITY_NAME] ?? "user_mention",
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react";
|
||||
import type { NodeViewProps } from "@tiptap/react";
|
||||
// local imports
|
||||
import { WorkItemEmbedExtensionConfig } from "./extension-config";
|
||||
import type { TWorkItemEmbedAttributes } from "./types";
|
||||
import { EWorkItemEmbedAttributeNames } from "./types";
|
||||
|
||||
type Props = {
|
||||
widgetCallback: ({
|
||||
@@ -18,15 +20,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={attrs[EWorkItemEmbedAttributeNames.ID]}>
|
||||
{props.widgetCallback({
|
||||
issueId: attrs[EWorkItemEmbedAttributeNames.ENTITY_IDENTIFIER] ?? "",
|
||||
projectId: attrs[EWorkItemEmbedAttributeNames.PROJECT_IDENTIFIER],
|
||||
workspaceSlug: attrs[EWorkItemEmbedAttributeNames.WORKSPACE_IDENTIFIER],
|
||||
})}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
15
packages/editor/src/core/extensions/work-item-embed/types.ts
Normal file
15
packages/editor/src/core/extensions/work-item-embed/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export enum EWorkItemEmbedAttributeNames {
|
||||
ID = "id",
|
||||
ENTITY_IDENTIFIER = "entity_identifier",
|
||||
PROJECT_IDENTIFIER = "project_identifier",
|
||||
WORKSPACE_IDENTIFIER = "workspace_identifier",
|
||||
ENTITY_NAME = "entity_name",
|
||||
}
|
||||
|
||||
export type TWorkItemEmbedAttributes = {
|
||||
[EWorkItemEmbedAttributeNames.ID]: string | undefined;
|
||||
[EWorkItemEmbedAttributeNames.ENTITY_IDENTIFIER]: string | undefined;
|
||||
[EWorkItemEmbedAttributeNames.PROJECT_IDENTIFIER]: string | undefined;
|
||||
[EWorkItemEmbedAttributeNames.WORKSPACE_IDENTIFIER]: string | undefined;
|
||||
[EWorkItemEmbedAttributeNames.ENTITY_NAME]: string | undefined;
|
||||
};
|
||||
Reference in New Issue
Block a user