[PE-69] fix: image restoration fixed for new images in private bucket (#5839)
* regression: image aspect ratio fix * fix: name of variables changed for clarity * fix: restore only on error * fix: restore image by handling it inside the image component * fix: image restoration fixed and aspect ratio added to old images to stop updates on load * fix: added back restoring logic for public images * fix: add conditions * fix: image attributes types * fix: return for old images * fix: remove passive false * fix: eslint fixes * fix: stopping infinite loading scenarios while restoring from error
This commit is contained in:
parent
e866571e04
commit
5a0dc4a65a
9 changed files with 98 additions and 55 deletions
|
|
@ -72,6 +72,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerRect = useRef<DOMRect | null>(null);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const [hasErroredOnFirstLoad, setHasErroredOnFirstLoad] = useState(false);
|
||||
const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false);
|
||||
|
||||
const updateAttributesSafely = useCallback(
|
||||
(attributes: Partial<ImageAttributes>, errorMessage: string) => {
|
||||
|
|
@ -145,8 +147,9 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
...prevSize,
|
||||
width: ensurePixelString(nodeWidth),
|
||||
height: ensurePixelString(nodeHeight),
|
||||
aspectRatio: nodeAspectRatio,
|
||||
}));
|
||||
}, [nodeWidth, nodeHeight]);
|
||||
}, [nodeWidth, nodeHeight, nodeAspectRatio]);
|
||||
|
||||
const handleResize = useCallback(
|
||||
(e: MouseEvent | TouchEvent) => {
|
||||
|
|
@ -159,7 +162,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
|
||||
setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` }));
|
||||
},
|
||||
[size]
|
||||
[size.aspectRatio]
|
||||
);
|
||||
|
||||
const handleResizeEnd = useCallback(() => {
|
||||
|
|
@ -182,11 +185,15 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
window.addEventListener("mousemove", handleResize);
|
||||
window.addEventListener("mouseup", handleResizeEnd);
|
||||
window.addEventListener("mouseleave", handleResizeEnd);
|
||||
window.addEventListener("touchmove", handleResize);
|
||||
window.addEventListener("touchend", handleResizeEnd);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleResize);
|
||||
window.removeEventListener("mouseup", handleResizeEnd);
|
||||
window.removeEventListener("mouseleave", handleResizeEnd);
|
||||
window.removeEventListener("touchmove", handleResize);
|
||||
window.removeEventListener("touchend", handleResizeEnd);
|
||||
};
|
||||
}
|
||||
}, [isResizing, handleResize, handleResizeEnd]);
|
||||
|
|
@ -203,7 +210,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
|
||||
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
|
||||
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
|
||||
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete;
|
||||
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
|
||||
// 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 = remoteImageSrc && 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)
|
||||
|
|
@ -231,9 +238,26 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
ref={imageRef}
|
||||
src={displayedImageSrc}
|
||||
onLoad={handleImageLoad}
|
||||
onError={(e) => {
|
||||
console.error("Error loading image", e);
|
||||
setFailedToLoadImage(true);
|
||||
onError={async (e) => {
|
||||
// for old image extension this command doesn't exist or if the image failed to load for the first time
|
||||
if (!editor?.commands.restoreImage || hasTriedRestoringImageOnce) {
|
||||
setFailedToLoadImage(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setHasErroredOnFirstLoad(true);
|
||||
// this is a type error from tiptap, don't remove await until it's fixed
|
||||
await editor?.commands.restoreImage?.(remoteImageSrc);
|
||||
imageRef.current.src = remoteImageSrc;
|
||||
} 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", {
|
||||
|
|
@ -284,6 +308,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
}
|
||||
)}
|
||||
onMouseDown={handleResizeStart}
|
||||
onTouchStart={handleResizeStart}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { Editor, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image";
|
||||
|
||||
export type CustomImageNodeViewProps = {
|
||||
export type CustomImageComponentProps = {
|
||||
getPos: () => number;
|
||||
editor: Editor;
|
||||
node: ProsemirrorNode & {
|
||||
node: NodeViewProps["node"] & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
updateAttributes: (attrs: Record<string, any>) => void;
|
||||
updateAttributes: (attrs: ImageAttributes) => void;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type CustomImageNodeViewProps = NodeViewProps & CustomImageComponentProps;
|
||||
|
||||
export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
||||
const { getPos, editor, node, updateAttributes, selected } = props;
|
||||
const { src: remoteImageSrc } = node.attrs;
|
||||
|
||||
const [isUploaded, setIsUploaded] = useState(false);
|
||||
const [imageFromFileSystem, setImageFromFileSystem] = useState<string | undefined>(undefined);
|
||||
|
|
@ -37,14 +39,13 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
|||
// the image is already uploaded if the image-component node has src attribute
|
||||
// and we need to remove the blob from our file system
|
||||
useEffect(() => {
|
||||
const remoteImageSrc = node.attrs.src;
|
||||
if (remoteImageSrc) {
|
||||
setIsUploaded(true);
|
||||
setImageFromFileSystem(undefined);
|
||||
} else {
|
||||
setIsUploaded(false);
|
||||
}
|
||||
}, [node.attrs.src]);
|
||||
}, [remoteImageSrc]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
|
|
@ -55,7 +56,7 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
|||
editorContainer={editorContainer}
|
||||
editor={editor}
|
||||
// @ts-expect-error function not expected here, but will still work
|
||||
src={editor?.commands?.getImageSource?.(node.attrs.src)}
|
||||
src={editor?.commands?.getImageSource?.(remoteImageSrc)}
|
||||
getPos={getPos}
|
||||
node={node}
|
||||
setEditorContainer={setEditorContainer}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,20 @@
|
|||
import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
// extensions
|
||||
import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image";
|
||||
import { type CustomImageComponentProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
|
||||
export const CustomImageUploader = (props: {
|
||||
editor: Editor;
|
||||
failedToLoadImage: boolean;
|
||||
getPos: () => number;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
type CustomImageUploaderProps = CustomImageComponentProps & {
|
||||
maxFileSize: number;
|
||||
node: ProsemirrorNode & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
selected: boolean;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
failedToLoadImage: boolean;
|
||||
setIsUploaded: (isUploaded: boolean) => void;
|
||||
updateAttributes: (attrs: Record<string, any>) => void;
|
||||
}) => {
|
||||
};
|
||||
|
||||
export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
const {
|
||||
editor,
|
||||
failedToLoadImage,
|
||||
|
|
@ -36,8 +29,8 @@ export const CustomImageUploader = (props: {
|
|||
// refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const hasTriggeredFilePickerRef = useRef(false);
|
||||
const { id: imageEntityId } = node.attrs;
|
||||
// derived values
|
||||
const imageEntityId = node.attrs.id;
|
||||
const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare module "@tiptap/core" {
|
|||
imageComponent: {
|
||||
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
|
||||
uploadImage: (file: File) => () => Promise<string> | undefined;
|
||||
restoreImage: (src: string) => () => Promise<void>;
|
||||
getImageSource?: (path: string) => () => string;
|
||||
};
|
||||
}
|
||||
|
|
@ -40,8 +41,8 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
const {
|
||||
getAssetSrc,
|
||||
upload,
|
||||
delete: deleteImage,
|
||||
restore: restoreImage,
|
||||
delete: deleteImageFn,
|
||||
restore: restoreImageFn,
|
||||
validation: { maxFileSize },
|
||||
} = props;
|
||||
|
||||
|
|
@ -85,22 +86,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
|
||||
|
|
@ -110,11 +95,29 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
|
||||
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
|
||||
];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImageFn(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
|
|
@ -179,6 +182,9 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
const fileUrl = await upload(file);
|
||||
return fileUrl;
|
||||
},
|
||||
restoreImage: (src: string) => async () => {
|
||||
await restoreImageFn(src);
|
||||
},
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -140,7 +140,10 @@ export const CoreEditorExtensions = (args: TArguments) => {
|
|||
if (editor.storage.imageComponent.uploadInProgress) return "";
|
||||
|
||||
const shouldHidePlaceholder =
|
||||
editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");
|
||||
editor.isActive("table") ||
|
||||
editor.isActive("codeBlock") ||
|
||||
editor.isActive("image") ||
|
||||
editor.isActive("imageComponent");
|
||||
|
||||
if (shouldHidePlaceholder) return "";
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import { CustomImageNode } from "@/extensions";
|
|||
|
||||
export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
const {
|
||||
delete: deleteImage,
|
||||
getAssetSrc,
|
||||
restore: restoreImage,
|
||||
delete: deleteImageFn,
|
||||
restore: restoreImageFn,
|
||||
validation: { maxFileSize },
|
||||
} = fileHandler;
|
||||
|
||||
|
|
@ -24,10 +24,11 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
|||
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
|
||||
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
|
||||
];
|
||||
},
|
||||
|
||||
|
|
@ -35,12 +36,14 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
|||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImage(src);
|
||||
await restoreImageFn(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
|
|
@ -65,6 +68,9 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
|||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ export const ImageExtensionWithoutProps = () =>
|
|||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">)
|
|||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor
|
|||
if (node.type.name !== nodeType) return;
|
||||
if (pos < 0 || pos > newState.doc.content.size) return;
|
||||
if (oldImageSources.has(node.attrs.src)) return;
|
||||
// if the src is just a id (private bucket), then we don't need to handle restore from here but
|
||||
// only while it fails to load
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
addedImages.push(node as ImageNode);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue