[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:
M. Palanikannan 2024-04-16 15:52:31 +05:30 committed by GitHub
parent 480aa906de
commit c18265c7cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 125 additions and 64 deletions

View File

@ -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,
() => ({

View File

@ -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();

View 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;
},
},
}),
];
},
});

View File

@ -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,
};
},

View File

@ -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",

View File

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

View File

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