forked from github/plane
[WEB-480] fix: image uploading with swr sync fixed for pages (#4187)
* fix: stroing the transactions in page * fix: page details changes * chore: page response change * chore: removed duplicated endpoints * chore: optimised the urls * chore: removed archived and favorite pages * chore: revamping pages store and components * mentions loading state part done * fixed mentions not showing in modals * removed comments and cleaned up types * removed unused types * reset: head * chore: pages store and component updates * style: pages list item UI * fix: improved colors and drag handle width * fix: slash commands are no more shown in the code blocks * fix: cleanup/hide drag handles post drop * fix: hide/cleanup drag handles post drag start * fix: aligning the drag handles better with the node post css changes of the length * fix: juggling back and forth of drag handles in ordered and unordered lists * chore: fix imports, ts errors and other things * fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes * chore: clearNodes after delete in case of selections being present * fix: hiding link selector in the bubble menu if inline code block is selected * chore: filtering, ordering and searching implemented * chore: updated pages store and updated UI * chore: new core editor just for document editor created * chore: removed setIsSubmitting prop in doc editor * fix: fixed submitting state for image uploads * refactor: setShouldShowAlert removed * refactor: rerenderOnPropsChange prop removed * chore: type inference magic in ref to expose an api for controlling editor menu items from outside * fix: naming imports * chore: change names of the exposed functions and removing old types * refactor: remove debouncedUpdatesEnabled prop; * refactor: editor heading markings now parsed using html * chore: removed unrelated components from the document editor * refactor: page details granular components * fix: remove onActionCompleteHandler * refactor: removed rerenderOnProps change prop * feat: added getMarkDown function * chore: update dropdown option actions * fix: sidebar markings update logic * chore: add image and to-do list actions to the toolbar * fix: handling refs and populating them via callbacks * feat: scroll to node api exposed * cleaning up editor refs when the editor is destroyed * feat: scrolling added to read only instance of the editor * fix: markings logic * fix: build errors with types * fix: build erros * fix: subscribing to transactions of editor via ref * chore: remove debug statements * fix: type errors * fix: temporary different slash commands for document editor * chore: inline code extension style * chore: remove border from readOnly editor * fix: editor bottom padding * chore: pages improvements * chore: handle Enter key on the page title * feat: added loading indicator logic in mentions * fix: mentions and slash commands now work well with multiple editors in one place * refactor: page store structure, filtering logic * feat: added better seperation in inline code blocks * feat: list autojoining added * fix: pages folder structure * fix: image refocus from external parts * working lists somewhat * chore: implement page reactions * fix: build errors * fix: build errors * fixed drag handles stuff * task list item fixed * working * fix: working on multiple nested lists * chore: remove debug statements * fix: Tab key on first list item handled to not go out of editor focus * feat: threshold auto scroll support added and multi nested list selection fixed * fix: caret color bug with improved inline code blocks * fix: node range error when bulk deleting with list * fix: removed slash commands from working in code blocks * chore: update typography margins * chore: new field added in page model * fix: better type inference in slash commands * chore: code block UI * feat: image insertion at correct position using ref added * feat: added improved mentions support for space * fix: type errors in mentions for comments in web app * sync: core with document-core * fix: build errors * fix: fallback for appendTo not being able to find active container instantly * fix: page store * fix: page description * fix: css quality issues * chore: code cleanup * chore: removed placeholder text in codeblocks * chore: archived pages response change * chore: archived pages response change * fix: initial pages list fetch * fix: pages list filters and ordering * chore: add access change option in the quick actions dropdown * fix: inline code block caret fixed * regression: removing extra text * chore: caret color removed * feat: copy code button added in code blocks * fix: initial load of page details * fix: initial load of page details * fix: image resizing weird behavior on click/expanding it too much fixed now * chore: copy page response * fix: todo list spacing * chore: description html in the copy page * chore: handle latest description on refetch * fix: saner scroll behaviours * fix: block menu positioning * fix: updated empty string description * feat: tab change sync support added * fix: infinite rerendering with markings * fix: block menu finally * fix: intial load on reload bug fixed * fix: nested lists alignment * fix: editor padding * fix: first level list items copyable * chore: list spacing * fix: title change * fix: pages list block items interaction * fix: saving chip position * fix: delete action from block menu to focus properly * fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node * style: table, chore: lite text editor toolbar * fix: page description tab sync * fix: lists spacing and alignment * refactor: document editor props * feat: rich text editor wrapper created and migrated core * feat: created wrapper around lite text editor and merged core * chore: add lite text editor toolbar * fix: build errors * fix: type errors and addead live updation of toolbar * chore: pages migration * fix: inbox issue * refactor: remove redundant package * refactor: unused files * fix: add dompurify to space app * fix: inline code margin * fix: editor className props * fix: build errors * fix: traversing up the tree before assuming the parent is not a list item * fix: drag handle positions for list items fixed * fix: removed focus at end logic after deleting block * fix: image wrapper overflow scroll fix with block menu's position * fix: selection and deletion logic for nested lists fixed!! * fix: hiding the block menu while scrolling in the document/app * fix: merge conflicts resolved from develop * fix: inbox issue description * chore: move page title to the web app * fix: handling edge cases for table selection * chore: lint issues * refactor: list item functions moved to same file * refactor: use mention hook * fix: added try catch blocks for mention suggestions * chore: remove unused code * fix: remove console logs * fix: remove console logs * fix: tracking image uploading status to prevent sync rerenders * chore: remove unnecessary props * fix: type of cancel button changed to button instead of default submit * feat: editor focus on saved position while syncing via swr --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
480aa906de
commit
c18265c7cf
@ -19,7 +19,7 @@ interface CustomEditorProps {
|
||||
uploadFile: UploadImage;
|
||||
restoreFile: RestoreImage;
|
||||
deleteFile: DeleteImage;
|
||||
cancelUploadImage?: () => any;
|
||||
cancelUploadImage?: () => void;
|
||||
initialValue: string;
|
||||
editorClassName: string;
|
||||
// undefined when prop is not passed, null if intentionally passed to stop
|
||||
@ -56,7 +56,7 @@ export const useEditor = ({
|
||||
}: CustomEditorProps) => {
|
||||
const editor = useCustomEditor({
|
||||
editorProps: {
|
||||
...CoreEditorProps(uploadFile, editorClassName),
|
||||
...CoreEditorProps(editorClassName),
|
||||
...editorProps,
|
||||
},
|
||||
extensions: [
|
||||
@ -69,6 +69,7 @@ export const useEditor = ({
|
||||
deleteFile,
|
||||
restoreFile,
|
||||
cancelUploadImage,
|
||||
uploadFile,
|
||||
},
|
||||
placeholder,
|
||||
}),
|
||||
@ -89,19 +90,37 @@ export const useEditor = ({
|
||||
},
|
||||
});
|
||||
|
||||
// for syncing swr data on tab refocus etc, can remove it once this is merged
|
||||
// https://github.com/ueberdosis/tiptap/pull/4453
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
|
||||
|
||||
// Inside your component or hook
|
||||
const savedSelectionRef = useRef(savedSelection);
|
||||
|
||||
// Update the ref whenever savedSelection changes
|
||||
useEffect(() => {
|
||||
savedSelectionRef.current = savedSelection;
|
||||
}, [savedSelection]);
|
||||
|
||||
// Effect for syncing SWR data
|
||||
useEffect(() => {
|
||||
// value is null when intentionally passed where syncing is not yet
|
||||
// supported and value is undefined when the data from swr is not populated
|
||||
if (value === null || value === undefined) return;
|
||||
if (editor && !editor.isDestroyed) editor?.commands.setContent(value);
|
||||
if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) {
|
||||
editor.commands.setContent(value);
|
||||
const currentSavedSelection = savedSelectionRef.current;
|
||||
if (currentSavedSelection) {
|
||||
editor.view.focus();
|
||||
const docLength = editor.state.doc.content.size;
|
||||
const relativePosition = Math.min(currentSavedSelection.from, docLength - 1);
|
||||
editor.commands.setTextSelection(relativePosition);
|
||||
} else {
|
||||
editor.commands.focus("end");
|
||||
}
|
||||
}
|
||||
}, [editor, value, id]);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
|
||||
|
||||
useImperativeHandle(
|
||||
forwardedRef,
|
||||
() => ({
|
||||
|
@ -126,7 +126,7 @@ export const insertImageCommand = (
|
||||
if (input.files?.length) {
|
||||
const file = input.files[0];
|
||||
const pos = savedSelection?.anchor ?? editor.view.state.selection.from;
|
||||
startImageUpload(file, editor.view, pos, uploadFile);
|
||||
startImageUpload(editor, file, editor.view, pos, uploadFile);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
|
45
packages/editor/core/src/ui/extensions/drop.tsx
Normal file
45
packages/editor/core/src/ui/extensions/drop.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "prosemirror-state";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
import { startImageUpload } from "../plugins/upload-image";
|
||||
|
||||
export const DropHandlerExtension = (uploadFile: UploadImage) =>
|
||||
Extension.create({
|
||||
name: "dropHandler",
|
||||
priority: 1000,
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("dropHandler"),
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.clipboardData.files[0];
|
||||
const pos = view.state.selection.from;
|
||||
startImageUpload(this.editor, file, view, pos, uploadFile);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handleDrop: (view, event, _slice, moved) => {
|
||||
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
if (coordinates) {
|
||||
startImageUpload(this.editor, file, view, coordinates.pos - 1, uploadFile);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
@ -18,7 +18,7 @@ interface ImageNode extends ProseMirrorNode {
|
||||
const deleteKey = new PluginKey("delete-image");
|
||||
const IMAGE_NODE_TYPE = "image";
|
||||
|
||||
export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
|
||||
export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => void) =>
|
||||
ImageExt.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
@ -28,7 +28,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
UploadImagesPlugin(cancelUploadImage),
|
||||
UploadImagesPlugin(this.editor, cancelUploadImage),
|
||||
new Plugin({
|
||||
key: deleteKey,
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
@ -124,6 +124,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
|
||||
addStorage() {
|
||||
return {
|
||||
images: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -29,6 +29,8 @@ import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline";
|
||||
import { CustomTypographyExtension } from "src/ui/extensions/typography";
|
||||
import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule";
|
||||
import { CustomCodeMarkPlugin } from "./custom-code-inline/inline-code-plugin";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
import { DropHandlerExtension } from "./drop";
|
||||
|
||||
type TArguments = {
|
||||
mentionConfig: {
|
||||
@ -38,14 +40,15 @@ type TArguments = {
|
||||
fileConfig: {
|
||||
deleteFile: DeleteImage;
|
||||
restoreFile: RestoreImage;
|
||||
cancelUploadImage?: () => any;
|
||||
cancelUploadImage?: () => void;
|
||||
uploadFile: UploadImage;
|
||||
};
|
||||
placeholder?: string | ((isFocused: boolean) => string);
|
||||
};
|
||||
|
||||
export const CoreEditorExtensions = ({
|
||||
mentionConfig,
|
||||
fileConfig: { deleteFile, restoreFile, cancelUploadImage },
|
||||
fileConfig: { deleteFile, restoreFile, cancelUploadImage, uploadFile },
|
||||
placeholder,
|
||||
}: TArguments) => [
|
||||
StarterKit.configure({
|
||||
@ -73,10 +76,8 @@ export const CoreEditorExtensions = ({
|
||||
width: 1,
|
||||
},
|
||||
}),
|
||||
// BulletList,
|
||||
// OrderedList,
|
||||
// ListItem,
|
||||
CustomQuoteExtension,
|
||||
DropHandlerExtension(uploadFile),
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
class: "my-4 border-custom-border-400",
|
||||
|
@ -1,12 +1,22 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
|
||||
const uploadKey = new PluginKey("upload-image");
|
||||
|
||||
export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
new Plugin({
|
||||
export const UploadImagesPlugin = (editor: Editor, cancelUploadImage?: () => void) => {
|
||||
let currentView: EditorView | null = null;
|
||||
return new Plugin({
|
||||
key: uploadKey,
|
||||
view(editorView) {
|
||||
currentView = editorView;
|
||||
return {
|
||||
destroy() {
|
||||
currentView = null;
|
||||
},
|
||||
};
|
||||
},
|
||||
state: {
|
||||
init() {
|
||||
return DecorationSet.empty;
|
||||
@ -27,13 +37,17 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
|
||||
// Create cancel button
|
||||
const cancelButton = document.createElement("button");
|
||||
cancelButton.type = "button";
|
||||
cancelButton.style.position = "absolute";
|
||||
cancelButton.style.right = "3px";
|
||||
cancelButton.style.top = "3px";
|
||||
cancelButton.setAttribute("class", "opacity-90 rounded-lg");
|
||||
|
||||
cancelButton.onclick = () => {
|
||||
cancelUploadImage?.();
|
||||
if (currentView) {
|
||||
cancelUploadImage?.();
|
||||
removePlaceholder(editor, currentView, id);
|
||||
}
|
||||
};
|
||||
|
||||
// Create an SVG element from the SVG string
|
||||
@ -59,6 +73,7 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function findPlaceholder(state: EditorState, id: {}) {
|
||||
const decos = uploadKey.getState(state);
|
||||
@ -66,26 +81,38 @@ function findPlaceholder(state: EditorState, id: {}) {
|
||||
return found.length ? found[0].from : null;
|
||||
}
|
||||
|
||||
const removePlaceholder = (view: EditorView, id: {}) => {
|
||||
const removePlaceholder = (editor: Editor, view: EditorView, id: {}) => {
|
||||
const removePlaceholderTr = view.state.tr.setMeta(uploadKey, {
|
||||
remove: { id },
|
||||
});
|
||||
view.dispatch(removePlaceholderTr);
|
||||
editor.storage.image.uploadInProgress = false;
|
||||
};
|
||||
|
||||
export async function startImageUpload(file: File, view: EditorView, pos: number, uploadFile: UploadImage) {
|
||||
export async function startImageUpload(
|
||||
editor: Editor,
|
||||
file: File,
|
||||
view: EditorView,
|
||||
pos: number,
|
||||
uploadFile: UploadImage
|
||||
) {
|
||||
editor.storage.image.uploadInProgress = true;
|
||||
|
||||
if (!file) {
|
||||
alert("No file selected. Please select a file to upload.");
|
||||
editor.storage.image.uploadInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.type.includes("image/")) {
|
||||
alert("Invalid file type. Please select an image file.");
|
||||
editor.storage.image.uploadInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert("File size too large. Please select a file smaller than 5MB.");
|
||||
editor.storage.image.uploadInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -110,7 +137,7 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
|
||||
// Handle FileReader errors
|
||||
reader.onerror = (error) => {
|
||||
console.error("FileReader error: ", error);
|
||||
removePlaceholder(view, id);
|
||||
removePlaceholder(editor, view, id);
|
||||
return;
|
||||
};
|
||||
|
||||
@ -121,7 +148,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
|
||||
const { schema } = view.state;
|
||||
pos = findPlaceholder(view.state, id);
|
||||
|
||||
if (pos == null) return;
|
||||
if (pos == null) {
|
||||
editor.storage.image.uploadInProgress = false;
|
||||
return;
|
||||
}
|
||||
const imageSrc = typeof src === "object" ? reader.result : src;
|
||||
|
||||
const node = schema.nodes.image.create({ src: imageSrc });
|
||||
@ -129,9 +159,9 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
|
||||
|
||||
view.dispatch(transaction);
|
||||
view.focus();
|
||||
editor.storage.image.uploadInProgress = false;
|
||||
} catch (error) {
|
||||
console.error("Upload error: ", error);
|
||||
removePlaceholder(view, id);
|
||||
removePlaceholder(editor, view, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { cn, findTableAncestor } from "src/lib/utils";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
import { startImageUpload } from "src/ui/plugins/upload-image";
|
||||
import { cn } from "src/lib/utils";
|
||||
|
||||
export function CoreEditorProps(uploadFile: UploadImage, editorClassName: string): EditorProps {
|
||||
export function CoreEditorProps(editorClassName: string): EditorProps {
|
||||
return {
|
||||
attributes: {
|
||||
class: cn(
|
||||
@ -17,45 +15,12 @@ export function CoreEditorProps(uploadFile: UploadImage, editorClassName: string
|
||||
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
||||
const slashCommand = document.querySelector("#slash-command");
|
||||
if (slashCommand) {
|
||||
console.log("registered");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
handlePaste: (view, event) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const selection: any = window?.getSelection();
|
||||
if (selection.rangeCount !== 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
if (findTableAncestor(range.startContainer)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.clipboardData.files[0];
|
||||
const pos = view.state.selection.from;
|
||||
startImageUpload(file, view, pos, uploadFile);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handleDrop: (view, event, _slice, moved) => {
|
||||
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
if (coordinates) {
|
||||
startImageUpload(file, view, coordinates.pos - 1, uploadFile);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/<img.*?>/g, "");
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user