Skip to content
Merged
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
3 changes: 2 additions & 1 deletion packages/editor/src/core/extensions/callout/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/core/extensions/callout/types.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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";
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/core/extensions/callout/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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"
Expand All @@ -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} />
Expand Down
9 changes: 9 additions & 0 deletions packages/editor/src/core/extensions/code/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum ECodeBlockAttributeNames {
ID = "id",
LANGUAGE = "language",
}

export type TCodeBlockAttributes = {
[ECodeBlockAttributeNames.ID]: string | null;
[ECodeBlockAttributeNames.LANGUAGE]: string | null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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",
Expand All @@ -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;
}

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");
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;
}
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;

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);
}
} 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
Expand All @@ -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"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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
Expand All @@ -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}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 14 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,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: ({
Expand All @@ -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 changes: 15 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,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;
};
Loading