From f679628365e78889755ea81c039b57994f1a595a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:36:06 +0530 Subject: [PATCH] [WIKI-449] feat: image block download and alignment options (#7254) * refactor: custom image extension * refactor: extension config * revert: image full screen component * fix: undo operation * chore: add download and alignment options * chore: render image full screen modal in a portal * chore: add missing attribute to image extension * chore: minor bugs and improvements * chore: add aria attributes * chore: remove unnecessary file * fix: full screen modal z-index --- packages/editor/package.json | 1 + .../custom-image/components/block.tsx | 222 ++++++++++-------- .../custom-image/components/node-view.tsx | 5 + .../components/toolbar/alignment.tsx | 63 +++++ .../components/toolbar/download.tsx | 24 ++ .../components/toolbar/full-screen/index.ts | 1 + .../modal.tsx} | 195 ++++++++------- .../components/toolbar/full-screen/root.tsx | 56 +++++ .../custom-image/components/toolbar/root.tsx | 44 ++-- .../core/extensions/custom-image/extension.ts | 3 +- .../src/core/extensions/custom-image/types.ts | 5 + .../src/core/extensions/custom-image/utils.ts | 25 +- .../extensions/image/extension-config.tsx | 3 + packages/editor/src/core/types/config.ts | 1 + space/app/layout.tsx | 1 + web/app/layout.tsx | 1 + web/core/hooks/editor/use-editor-config.ts | 16 +- 17 files changed, 456 insertions(+), 210 deletions(-) create mode 100644 packages/editor/src/core/extensions/custom-image/components/toolbar/alignment.tsx create mode 100644 packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx create mode 100644 packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/index.ts rename packages/editor/src/core/extensions/custom-image/components/toolbar/{full-screen.tsx => full-screen/modal.tsx} (57%) create mode 100644 packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx diff --git a/packages/editor/package.json b/packages/editor/package.json index f6118c22f6..d1d8543367 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -39,6 +39,7 @@ "@headlessui/react": "^1.7.3", "@hocuspocus/provider": "^2.15.0", "@plane/constants": "*", + "@plane/hooks": "*", "@plane/types": "*", "@plane/ui": "*", "@plane/utils": "*", diff --git a/packages/editor/src/core/extensions/custom-image/components/block.tsx b/packages/editor/src/core/extensions/custom-image/components/block.tsx index c895d19ccf..45c66f428b 100644 --- a/packages/editor/src/core/extensions/custom-image/components/block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/block.tsx @@ -17,6 +17,7 @@ type CustomImageBlockProps = CustomImageNodeViewProps & { setEditorContainer: (editorContainer: HTMLDivElement | null) => void; setFailedToLoadImage: (isError: boolean) => void; src: string | undefined; + downloadSrc: string | undefined; }; export const CustomImageBlock: React.FC = (props) => { @@ -32,9 +33,16 @@ export const CustomImageBlock: React.FC = (props) => { setEditorContainer, setFailedToLoadImage, src: resolvedImageSrc, + downloadSrc: resolvedDownloadSrc, updateAttributes, } = props; - const { width: nodeWidth, height: nodeHeight, aspectRatio: nodeAspectRatio, src: imgNodeSrc } = node.attrs; + const { + width: nodeWidth, + height: nodeHeight, + aspectRatio: nodeAspectRatio, + src: imgNodeSrc, + alignment: nodeAlignment, + } = node.attrs; // states const [size, setSize] = useState({ width: ensurePixelString(nodeWidth, "35%") ?? "35%", @@ -131,12 +139,17 @@ export const CustomImageBlock: React.FC = (props) => { const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; - const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE); - const newHeight = newWidth / size.aspectRatio; - - setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` })); + if (nodeAlignment === "right") { + const newWidth = Math.max(containerRect.current.right - clientX, MIN_SIZE); + const newHeight = newWidth / size.aspectRatio; + setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` })); + } else { + const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE); + const newHeight = newWidth / size.aspectRatio; + setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` })); + } }, - [size.aspectRatio] + [nodeAlignment, size.aspectRatio] ); const handleResizeEnd = useCallback(() => { @@ -188,7 +201,7 @@ export const CustomImageBlock: React.FC = (props) => { // show the image upload status only when the resolvedImageSrc is not ready const showUploadStatus = !resolvedImageSrc; // show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) - const showImageUtils = resolvedImageSrc && initialResizeComplete; + const showImageToolbar = resolvedImageSrc && resolvedDownloadSrc && initialResizeComplete; // show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) const showImageResizer = editor.isEditable && resolvedImageSrc && initialResizeComplete; // show the preview image from the file system if the remote image's src is not set @@ -197,108 +210,117 @@ export const CustomImageBlock: React.FC = (props) => { return (
- {showImageLoader && ( -
- )} - { - // 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"); - } - if (!resolvedImageSrc) { - throw new Error("No resolved image source available"); - } - 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 - hidden: showImageLoader, - "read-only-image": !editor.isEditable, - "blur-sm opacity-80 loading-image": !resolvedImageSrc, - })} +
- {showUploadStatus && node.attrs.id && } - {showImageUtils && ( - + {showImageLoader && ( +
+ )} + { + // 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"); + } + if (!resolvedImageSrc) { + throw new Error("No resolved image source available"); + } + 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 + hidden: showImageLoader, + "read-only-image": !editor.isEditable, + "blur-sm opacity-80 loading-image": !resolvedImageSrc, + })} + style={{ width: size.width, - height: size.height, - aspectRatio: size.aspectRatio === null ? 1 : size.aspectRatio, - src: resolvedImageSrc, + ...(size.aspectRatio && { aspectRatio: size.aspectRatio }), }} /> - )} - {selected && displayedImageSrc === resolvedImageSrc && ( -
- )} - {showImageResizer && ( - <> -
} + {showImageToolbar && ( + + updateAttributesSafely({ alignment }, "Failed to update attributes while changing alignment:") + } /> -
- - )} + )} + {selected && displayedImageSrc === resolvedImageSrc && ( +
+ )} + {showImageResizer && ( + <> +
+
+ + )} +
); }; diff --git a/packages/editor/src/core/extensions/custom-image/components/node-view.tsx b/packages/editor/src/core/extensions/custom-image/components/node-view.tsx index 74ea2c38c5..eb38ab4054 100644 --- a/packages/editor/src/core/extensions/custom-image/components/node-view.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/node-view.tsx @@ -26,6 +26,7 @@ export const CustomImageNodeView: React.FC = (props) = const [isUploaded, setIsUploaded] = useState(false); const [resolvedSrc, setResolvedSrc] = useState(undefined); + const [resolvedDownloadSrc, setResolvedDownloadSrc] = useState(undefined); const [imageFromFileSystem, setImageFromFileSystem] = useState(undefined); const [failedToLoadImage, setFailedToLoadImage] = useState(false); @@ -53,12 +54,15 @@ export const CustomImageNodeView: React.FC = (props) = useEffect(() => { if (!imgNodeSrc) { setResolvedSrc(undefined); + setResolvedDownloadSrc(undefined); return; } const getImageSource = async () => { const url = await extension.options.getImageSource?.(imgNodeSrc); setResolvedSrc(url); + const downloadUrl = await extension.options.getImageDownloadSource?.(imgNodeSrc); + setResolvedDownloadSrc(downloadUrl); }; getImageSource(); }, [imgNodeSrc, extension.options]); @@ -73,6 +77,7 @@ export const CustomImageNodeView: React.FC = (props) = setEditorContainer={setEditorContainer} setFailedToLoadImage={setFailedToLoadImage} src={resolvedSrc} + downloadSrc={resolvedDownloadSrc} {...props} /> ) : ( diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/alignment.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/alignment.tsx new file mode 100644 index 0000000000..3790fa5471 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/alignment.tsx @@ -0,0 +1,63 @@ +import { ChevronDown } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +// plane imports +import { useOutsideClickDetector } from "@plane/hooks"; +import { Tooltip } from "@plane/ui"; +// local imports +import type { TCustomImageAlignment } from "../../types"; +import { IMAGE_ALIGNMENT_OPTIONS } from "../../utils"; + +type Props = { + activeAlignment: TCustomImageAlignment; + handleChange: (alignment: TCustomImageAlignment) => void; + toggleToolbarViewStatus: (val: boolean) => void; +}; + +export const ImageAlignmentAction: React.FC = (props) => { + const { activeAlignment, handleChange, toggleToolbarViewStatus } = props; + // states + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + // refs + const dropdownRef = useRef(null); + // derived values + const activeAlignmentDetails = IMAGE_ALIGNMENT_OPTIONS.find((option) => option.value === activeAlignment); + + useOutsideClickDetector(dropdownRef, () => setIsDropdownOpen(false)); + + useEffect(() => { + toggleToolbarViewStatus(isDropdownOpen); + }, [isDropdownOpen, toggleToolbarViewStatus]); + + return ( +
+ + + + {isDropdownOpen && ( +
+ {IMAGE_ALIGNMENT_OPTIONS.map((option) => ( + + + + ))} +
+ )} +
+ ); +}; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx new file mode 100644 index 0000000000..2f0a665ae5 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx @@ -0,0 +1,24 @@ +import { Download } from "lucide-react"; +// plane imports +import { Tooltip } from "@plane/ui"; + +type Props = { + src: string; +}; + +export const ImageDownloadAction: React.FC = (props) => { + const { src } = props; + + return ( + + + + ); +}; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/index.ts b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx similarity index 57% rename from packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx rename to packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx index 1d2e52ca01..9a30908c2c 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx @@ -1,37 +1,39 @@ -import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState, useRef } from "react"; +import { Download, ExternalLink, Minus, Plus, X } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import ReactDOM from "react-dom"; // plane imports import { cn } from "@plane/utils"; -type Props = { - image: { - width: string; - height: string; - aspectRatio: number; - src: string; - }; - isOpen: boolean; - toggleFullScreenMode: (val: boolean) => void; -}; - const MIN_ZOOM = 0.5; const MAX_ZOOM = 2; const ZOOM_SPEED = 0.05; const ZOOM_STEPS = [0.5, 1, 1.5, 2]; -export const ImageFullScreenAction: React.FC = (props) => { - const { image, isOpen: isFullScreenEnabled, toggleFullScreenMode } = props; - const { src, width, aspectRatio } = image; +type Props = { + aspectRatio: number; + isFullScreenEnabled: boolean; + downloadSrc: string; + src: string; + toggleFullScreenMode: (val: boolean) => void; + width: string; +}; + +const ImageFullScreenModalWithoutPortal = (props: Props) => { + const { aspectRatio, isFullScreenEnabled, downloadSrc, src, toggleFullScreenMode, width } = props; + // refs + const dragStart = useRef({ x: 0, y: 0 }); + const dragOffset = useRef({ x: 0, y: 0 }); const [magnification, setMagnification] = useState(1); const [initialMagnification, setInitialMagnification] = useState(1); const [isDragging, setIsDragging] = useState(false); - const dragStart = useRef({ x: 0, y: 0 }); - const dragOffset = useRef({ x: 0, y: 0 }); const modalRef = useRef(null); const imgRef = useRef(null); - const widthInNumber = useMemo(() => Number(width?.replace("px", "")), [width]); + const widthInNumber = useMemo(() => { + if (!width) return 0; + return Number(width.replace("px", "")); + }, [width]); const setImageRef = useCallback( (node: HTMLImageElement | null) => { @@ -148,7 +150,7 @@ export const ImageFullScreenAction: React.FC = (props) => { e.preventDefault(); // Handle pinch-to-zoom - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { const delta = e.deltaY; setMagnification((prev) => { const newZoom = prev * (1 - delta * ZOOM_SPEED); @@ -165,7 +167,7 @@ export const ImageFullScreenAction: React.FC = (props) => { return; } }, - [isFullScreenEnabled, magnification] + [isFullScreenEnabled] ); // Event listeners @@ -185,84 +187,99 @@ export const ImageFullScreenAction: React.FC = (props) => { }; }, [isFullScreenEnabled, handleKeyDown, handleMouseMove, handleMouseUp, handleWheel]); + if (!isFullScreenEnabled) return null; + return ( - <> +
e.target === modalRef.current && handleClose()} + className="relative size-full grid place-items-center overflow-hidden" > -
e.target === modalRef.current && handleClose()} - className="relative size-full grid place-items-center overflow-hidden" + - -
-
- - {Math.round(100 * magnification)}% - -
+ + + +
+
+ {Math.round(100 * magnification)}% +
+ +
- - +
); }; + +export const ImageFullScreenModal: React.FC = (props) => { + let modal = ; + const portal = document.querySelector("#editor-portal"); + if (portal) { + modal = ReactDOM.createPortal(modal, portal); + } else { + console.warn("Portal element #editor-portal not found. Rendering inline."); + } + return modal; +}; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx new file mode 100644 index 0000000000..2108bfeaae --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx @@ -0,0 +1,56 @@ +import { Maximize } from "lucide-react"; +import { useEffect, useState } from "react"; +// plane imports +import { Tooltip } from "@plane/ui"; +// local imports +import { ImageFullScreenModal } from "./modal"; + +type Props = { + image: { + downloadSrc: string; + src: string; + height: string; + width: string; + aspectRatio: number; + }; + toggleToolbarViewStatus: (val: boolean) => void; +}; + +export const ImageFullScreenActionRoot: React.FC = (props) => { + const { image, toggleToolbarViewStatus } = props; + // states + const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); + // derived values + const { downloadSrc, src, width, aspectRatio } = image; + + useEffect(() => { + toggleToolbarViewStatus(isFullScreenEnabled); + }, [isFullScreenEnabled, toggleToolbarViewStatus]); + + return ( + <> + + + + + + ); +}; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx index f9cd28d48d..06277fa252 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx @@ -2,35 +2,43 @@ import { useState } from "react"; // plane imports import { cn } from "@plane/utils"; // local imports -import { ImageFullScreenAction } from "./full-screen"; +import type { TCustomImageAlignment } from "../../types"; +import { ImageAlignmentAction } from "./alignment"; +import { ImageDownloadAction } from "./download"; +import { ImageFullScreenActionRoot } from "./full-screen"; type Props = { - containerClassName?: string; - image: { - width: string; - height: string; - aspectRatio: number; - src: string; - }; + alignment: TCustomImageAlignment; + width: string; + height: string; + aspectRatio: number; + src: string; + downloadSrc: string; + handleAlignmentChange: (alignment: TCustomImageAlignment) => void; }; export const ImageToolbarRoot: React.FC = (props) => { - const { containerClassName, image } = props; - // state - const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); + const { alignment, downloadSrc, handleAlignmentChange } = props; + // states + const [shouldShowToolbar, setShouldShowToolbar] = useState(false); return ( <>
- setIsFullScreenEnabled(val)} + + +
); diff --git a/packages/editor/src/core/extensions/custom-image/extension.ts b/packages/editor/src/core/extensions/custom-image/extension.ts index ec795da842..3ce93ecd87 100644 --- a/packages/editor/src/core/extensions/custom-image/extension.ts +++ b/packages/editor/src/core/extensions/custom-image/extension.ts @@ -20,7 +20,7 @@ type Props = { export const CustomImageExtension = (props: Props) => { const { fileHandler, isEditable } = props; // derived values - const { getAssetSrc, restore: restoreImageFn } = fileHandler; + const { getAssetSrc, getAssetDownloadSrc, restore: restoreImageFn } = fileHandler; return CustomImageExtensionConfig.extend({ selectable: isEditable, @@ -31,6 +31,7 @@ export const CustomImageExtension = (props: Props) => { return { ...this.parent?.(), + getImageDownloadSource: getAssetDownloadSrc, getImageSource: getAssetSrc, restoreImage: restoreImageFn, uploadImage: upload, diff --git a/packages/editor/src/core/extensions/custom-image/types.ts b/packages/editor/src/core/extensions/custom-image/types.ts index 675d8a2215..4ed5cd6ce4 100644 --- a/packages/editor/src/core/extensions/custom-image/types.ts +++ b/packages/editor/src/core/extensions/custom-image/types.ts @@ -8,6 +8,7 @@ export enum ECustomImageAttributeNames { HEIGHT = "height", ASPECT_RATIO = "aspectRatio", SOURCE = "src", + ALIGNMENT = "alignment", } export type Pixel = `${number}px`; @@ -20,12 +21,15 @@ export type TCustomImageSize = { aspectRatio: number | null; }; +export type TCustomImageAlignment = "left" | "center" | "right"; + export type TCustomImageAttributes = { [ECustomImageAttributeNames.ID]: string | null; [ECustomImageAttributeNames.WIDTH]: PixelAttribute<"35%" | number> | null; [ECustomImageAttributeNames.HEIGHT]: PixelAttribute<"auto" | number> | null; [ECustomImageAttributeNames.ASPECT_RATIO]: number | null; [ECustomImageAttributeNames.SOURCE]: string | null; + [ECustomImageAttributeNames.ALIGNMENT]: TCustomImageAlignment; }; export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean }; @@ -37,6 +41,7 @@ export type InsertImageComponentProps = { }; export type CustomImageExtensionOptions = { + getImageDownloadSource: TFileHandler["getAssetDownloadSrc"]; getImageSource: TFileHandler["getAssetSrc"]; restoreImage: TFileHandler["restore"]; uploadImage?: TFileHandler["upload"]; diff --git a/packages/editor/src/core/extensions/custom-image/utils.ts b/packages/editor/src/core/extensions/custom-image/utils.ts index 88d8032c72..10f2bb1e2c 100644 --- a/packages/editor/src/core/extensions/custom-image/utils.ts +++ b/packages/editor/src/core/extensions/custom-image/utils.ts @@ -1,10 +1,11 @@ import type { Editor } from "@tiptap/core"; +import { AlignCenter, AlignLeft, AlignRight, type LucideIcon } from "lucide-react"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { getExtensionStorage } from "@/helpers/get-extension-storage"; // local imports -import { ECustomImageAttributeNames, type Pixel, type TCustomImageAttributes } from "./types"; +import { ECustomImageAttributeNames, TCustomImageAlignment, type Pixel, type TCustomImageAttributes } from "./types"; export const DEFAULT_CUSTOM_IMAGE_ATTRIBUTES: TCustomImageAttributes = { [ECustomImageAttributeNames.SOURCE]: null, @@ -12,6 +13,7 @@ export const DEFAULT_CUSTOM_IMAGE_ATTRIBUTES: TCustomImageAttributes = { [ECustomImageAttributeNames.WIDTH]: "35%", [ECustomImageAttributeNames.HEIGHT]: "auto", [ECustomImageAttributeNames.ASPECT_RATIO]: null, + [ECustomImageAttributeNames.ALIGNMENT]: "left", }; export const getImageComponentImageFileMap = (editor: Editor) => @@ -32,4 +34,25 @@ export const ensurePixelString = ( return value; }; +export const IMAGE_ALIGNMENT_OPTIONS: { + label: string; + value: TCustomImageAlignment; + icon: LucideIcon; +}[] = [ + { + label: "Left", + value: "left", + icon: AlignLeft, + }, + { + label: "Center", + value: "center", + icon: AlignCenter, + }, + { + label: "Right", + value: "right", + icon: AlignRight, + }, +]; export const getImageBlockId = (id: string) => `editor-image-block-${id}`; diff --git a/packages/editor/src/core/extensions/image/extension-config.tsx b/packages/editor/src/core/extensions/image/extension-config.tsx index 6dbad2d24d..7456e3dfb0 100644 --- a/packages/editor/src/core/extensions/image/extension-config.tsx +++ b/packages/editor/src/core/extensions/image/extension-config.tsx @@ -19,6 +19,9 @@ export const ImageExtensionConfig = BaseImageExtension.extend< aspectRatio: { default: null, }, + alignment: { + default: "left", + }, }; }, }); diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts index 60ccfa8412..7ef685ad02 100644 --- a/packages/editor/src/core/types/config.ts +++ b/packages/editor/src/core/types/config.ts @@ -3,6 +3,7 @@ import { TWebhookConnectionQueryParams } from "@plane/types"; export type TReadOnlyFileHandler = { checkIfAssetExists: (assetId: string) => Promise; + getAssetDownloadSrc: (path: string) => Promise; getAssetSrc: (path: string) => Promise; restore: (assetSrc: string) => Promise; }; diff --git a/space/app/layout.tsx b/space/app/layout.tsx index 96a1922737..d0c7435da9 100644 --- a/space/app/layout.tsx +++ b/space/app/layout.tsx @@ -33,6 +33,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) +
<>{children} diff --git a/web/app/layout.tsx b/web/app/layout.tsx index fc454e54bb..433dea7f91 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -80,6 +80,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
+
{ const res = await fileService.checkIfAssetExists(workspaceSlug, assetId); return res?.exists ?? false; }, + getAssetDownloadSrc: async (path) => { + if (!path) return ""; + if (path?.startsWith("http")) { + return path; + } else { + return ( + getEditorAssetDownloadSrc({ + assetId: path, + projectId, + workspaceSlug, + }) ?? "" + ); + } + }, getAssetSrc: async (path) => { if (!path) return ""; if (path?.startsWith("http")) {