forked from github/plane
fix: Image resize, Link selector in Modals, Delete/ sync images and so much more (#1896)
* added image-resizing support * link form removed * updated image upload logic and 35% default width on upload * removed shadow, added alert if not saved and ts errors * prevent enter key on Link selector to trigger modal submit * added workspace slug to all tiptap instances * added delete plugin with loading indicator * added better syncing of "Saved" state of editor and Image uploads * removed redundant description_html check
This commit is contained in:
parent
cebc8bdc8d
commit
d470adf262
@ -140,14 +140,14 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
||||
isOpen ? "block" : "hidden"
|
||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
||||
<div className="text-sm">
|
||||
Content:
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={htmlContent ?? `<p>${content}</p>`}
|
||||
customClassName="-m-3"
|
||||
noBorder
|
||||
@ -161,6 +161,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
<div className="page-block-section text-sm">
|
||||
Response:
|
||||
<Tiptap
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={`<p>${response}</p>`}
|
||||
customClassName="-mx-3 -my-3"
|
||||
noBorder
|
||||
@ -179,8 +180,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
type="text"
|
||||
name="task"
|
||||
register={register}
|
||||
placeholder={`${
|
||||
content && content !== ""
|
||||
placeholder={`${content && content !== ""
|
||||
? "Tell AI what action to perform on this content..."
|
||||
: "Ask AI anything..."
|
||||
}`}
|
||||
|
@ -293,6 +293,7 @@ export const InboxMainContent: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<IssueDescriptionForm
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
issue={{
|
||||
name: issueDetails.name,
|
||||
description: issueDetails.description,
|
||||
|
@ -175,6 +175,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-4">
|
||||
<CommentCard
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
comment={activityItem as IIssueComment}
|
||||
onSubmit={handleCommentUpdate}
|
||||
handleCommentDeletion={handleCommentDelete}
|
||||
|
@ -93,6 +93,7 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={
|
||||
!value ||
|
||||
|
@ -22,12 +22,13 @@ const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEdit
|
||||
TiptapEditor.displayName = "TiptapEditor";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
comment: IIssueComment;
|
||||
onSubmit: (comment: IIssueComment) => void;
|
||||
handleCommentDeletion: (comment: string) => void;
|
||||
};
|
||||
|
||||
export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => {
|
||||
export const CommentCard: React.FC<Props> = ({ comment, workspaceSlug, onSubmit, handleCommentDeletion }) => {
|
||||
const { user } = useUser();
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
@ -109,6 +110,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
>
|
||||
<div>
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html")}
|
||||
debouncedUpdatesEnabled={false}
|
||||
@ -138,6 +140,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html}
|
||||
editable={false}
|
||||
|
@ -24,6 +24,7 @@ export interface IssueDetailsProps {
|
||||
description: string;
|
||||
description_html: string;
|
||||
};
|
||||
workspaceSlug: string;
|
||||
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
|
||||
isAllowed: boolean;
|
||||
}
|
||||
@ -31,6 +32,7 @@ export interface IssueDetailsProps {
|
||||
export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
issue,
|
||||
handleFormSubmit,
|
||||
workspaceSlug,
|
||||
isAllowed,
|
||||
}) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
@ -69,11 +71,15 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting === "submitted") {
|
||||
setShowAlert(false);
|
||||
setTimeout(async () => {
|
||||
setIsSubmitting("saved");
|
||||
}, 2000);
|
||||
} else if (isSubmitting === "submitting") {
|
||||
setShowAlert(true);
|
||||
}
|
||||
}, [isSubmitting]);
|
||||
}, [isSubmitting, setShowAlert]);
|
||||
|
||||
|
||||
// reset form values
|
||||
useEffect(() => {
|
||||
@ -112,8 +118,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
{characterLimit && (
|
||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
|
||||
<span
|
||||
className={`${
|
||||
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
||||
className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{watch("name").length}
|
||||
@ -139,11 +144,14 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
? watch("description_html")
|
||||
: value
|
||||
}
|
||||
workspaceSlug={workspaceSlug}
|
||||
debouncedUpdatesEnabled={true}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
customClassName="min-h-[150px] shadow-sm"
|
||||
editorContentCustomClassNames="pb-9"
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
setIsSubmitting("submitting");
|
||||
onChange(description_html);
|
||||
setValue("description", description);
|
||||
@ -156,8 +164,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
}`}
|
||||
>
|
||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||
|
@ -370,6 +370,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
|
||||
return (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
value={
|
||||
|
@ -124,6 +124,7 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
</div>
|
||||
) : null}
|
||||
<IssueDescriptionForm
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
issue={issueDetails}
|
||||
handleFormSubmit={submitChanges}
|
||||
isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable}
|
||||
|
@ -292,6 +292,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
if (!data)
|
||||
return (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={"<p></p>"}
|
||||
debouncedUpdatesEnabled={false}
|
||||
@ -311,12 +312,11 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
ref={editorRef}
|
||||
value={
|
||||
value && value !== "" && Object.keys(value).length > 0
|
||||
? value
|
||||
: watch("description_html") && watch("description_html") !== ""
|
||||
? watch("description_html")
|
||||
: { type: "doc", content: [{ type: "paragraph" }] }
|
||||
}
|
||||
debouncedUpdatesEnabled={false}
|
||||
@ -334,8 +334,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
<div className="m-2 mt-6 flex">
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||
iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
|
@ -456,6 +456,7 @@ export const SinglePageBlock: React.FC<Props> = ({
|
||||
{showBlockDetails
|
||||
? block.description_html.length > 7 && (
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={block.description_html}
|
||||
customClassName="text-sm min-h-[150px]"
|
||||
noBorder
|
||||
|
@ -1,17 +1,27 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Check, Trash } from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
|
||||
import { cn } from "../utils";
|
||||
|
||||
import isValidHttpUrl from "./utils/link-validator";
|
||||
interface LinkSelectorProps {
|
||||
editor: Editor;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
|
||||
export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onLinkSubmit = useCallback(() => {
|
||||
const input = inputRef.current;
|
||||
const url = input?.value;
|
||||
if (url && isValidHttpUrl(url)) {
|
||||
editor.chain().focus().setLink({ href: url }).run();
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [editor, inputRef, setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current && inputRef.current?.focus();
|
||||
});
|
||||
@ -38,15 +48,13 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
||||
</p>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const input = form.elements[0] as HTMLInputElement;
|
||||
editor.chain().focus().setLink({ href: input.value }).run();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
<div
|
||||
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault(); onLinkSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
@ -57,6 +65,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
||||
/>
|
||||
{editor.getAttributes("link").href ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
|
||||
onClick={() => {
|
||||
editor.chain().focus().unsetLink().run();
|
||||
@ -66,11 +75,15 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
||||
<Trash className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90">
|
||||
<button className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90" type="button"
|
||||
onClick={() => {
|
||||
onLinkSubmit();
|
||||
}}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,12 @@
|
||||
export default function isValidHttpUrl(string: string): boolean {
|
||||
let url;
|
||||
|
||||
try {
|
||||
url = new URL(string);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
}
|
||||
|
57
apps/app/components/tiptap/extensions/image-resize.tsx
Normal file
57
apps/app/components/tiptap/extensions/image-resize.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import Moveable from "react-moveable";
|
||||
|
||||
export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
const updateMediaSize = () => {
|
||||
const imageInfo = document.querySelector(
|
||||
".ProseMirror-selectednode",
|
||||
) as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const selection = editor.state.selection;
|
||||
editor.commands.setImage({
|
||||
src: imageInfo.src,
|
||||
width: Number(imageInfo.style.width.replace("px", "")),
|
||||
height: Number(imageInfo.style.height.replace("px", "")),
|
||||
} as any);
|
||||
editor.commands.setNodeSelection(selection.from);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Moveable
|
||||
target={document.querySelector(".ProseMirror-selectednode") as any}
|
||||
container={null}
|
||||
origin={false}
|
||||
edge={false}
|
||||
throttleDrag={0}
|
||||
keepRatio={true}
|
||||
resizable={true}
|
||||
throttleResize={0}
|
||||
onResize={({
|
||||
target,
|
||||
width,
|
||||
height,
|
||||
delta,
|
||||
}:
|
||||
any) => {
|
||||
delta[0] && (target!.style.width = `${width}px`);
|
||||
delta[1] && (target!.style.height = `${height}px`);
|
||||
}}
|
||||
onResizeEnd={() => {
|
||||
updateMediaSize();
|
||||
}}
|
||||
scalable={true}
|
||||
renderDirections={["w", "e"]}
|
||||
onScale={({
|
||||
target,
|
||||
transform,
|
||||
}:
|
||||
any) => {
|
||||
target!.style.transform = transform;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
import TiptapImage from "@tiptap/extension-image";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import TiptapUnderline from "@tiptap/extension-underline";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
@ -18,18 +17,13 @@ import { InputRule } from "@tiptap/core";
|
||||
import ts from "highlight.js/lib/languages/typescript";
|
||||
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import UploadImagesPlugin from "../plugins/upload-image";
|
||||
import UniqueID from "@tiptap-pro/extension-unique-id";
|
||||
import UpdatedImage from "./updated-image";
|
||||
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
||||
|
||||
lowlight.registerLanguage("ts", ts);
|
||||
|
||||
const CustomImage = TiptapImage.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [UploadImagesPlugin()];
|
||||
},
|
||||
});
|
||||
|
||||
export const TiptapExtensions = [
|
||||
export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
@ -93,13 +87,14 @@ export const TiptapExtensions = [
|
||||
},
|
||||
}),
|
||||
TiptapLink.configure({
|
||||
protocols: ["http", "https"],
|
||||
validate: (url) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
CustomImage.configure({
|
||||
allowBase64: true,
|
||||
UpdatedImage.configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-lg border border-custom-border-300",
|
||||
},
|
||||
@ -117,7 +112,7 @@ export const TiptapExtensions = [
|
||||
UniqueID.configure({
|
||||
types: ["image"],
|
||||
}),
|
||||
SlashCommand,
|
||||
SlashCommand(workspaceSlug, setIsSubmitting),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
Color,
|
||||
|
22
apps/app/components/tiptap/extensions/updated-image.tsx
Normal file
22
apps/app/components/tiptap/extensions/updated-image.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import Image from "@tiptap/extension-image";
|
||||
import TrackImageDeletionPlugin from "../plugins/delete-image";
|
||||
import UploadImagesPlugin from "../plugins/upload-image";
|
||||
|
||||
const UpdatedImage = Image.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [UploadImagesPlugin(), TrackImageDeletionPlugin()];
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: '35%',
|
||||
},
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default UpdatedImage;
|
@ -1,14 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { EditorBubbleMenu } from "./bubble-menu";
|
||||
import { TiptapExtensions } from "./extensions";
|
||||
import { TiptapEditorProps } from "./props";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { Editor as CoreEditor } from "@tiptap/core";
|
||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
import fileService from "services/file.service";
|
||||
import { useImperativeHandle, useRef } from "react";
|
||||
import { ImageResizer } from "./extensions/image-resize";
|
||||
|
||||
export interface ITiptapRichTextEditor {
|
||||
value: string;
|
||||
@ -18,6 +14,8 @@ export interface ITiptapRichTextEditor {
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
workspaceSlug: string;
|
||||
editable?: boolean;
|
||||
forwardedRef?: any;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
@ -30,22 +28,24 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
||||
forwardedRef,
|
||||
editable,
|
||||
setIsSubmitting,
|
||||
setShouldShowAlert,
|
||||
editorContentCustomClassNames,
|
||||
value,
|
||||
noBorder,
|
||||
workspaceSlug,
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
} = props;
|
||||
|
||||
const editor = useEditor({
|
||||
editable: editable ?? true,
|
||||
editorProps: TiptapEditorProps,
|
||||
extensions: TiptapExtensions,
|
||||
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
|
||||
extensions: TiptapExtensions(workspaceSlug, setIsSubmitting),
|
||||
content: value,
|
||||
onUpdate: async ({ editor }) => {
|
||||
// for instant feedback loop
|
||||
setIsSubmitting?.("submitting");
|
||||
checkForNodeDeletions(editor);
|
||||
setShouldShowAlert?.(true);
|
||||
if (debouncedUpdatesEnabled) {
|
||||
debouncedUpdates({ onChange, editor });
|
||||
} else {
|
||||
@ -65,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
||||
},
|
||||
}));
|
||||
|
||||
const previousState = useRef<EditorState>();
|
||||
|
||||
const onNodeDeleted = useCallback(async (node: Node) => {
|
||||
if (node.type.name === "image") {
|
||||
const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
|
||||
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
||||
if (resStatus === 204) {
|
||||
console.log("file deleted successfully");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkForNodeDeletions = useCallback(
|
||||
(editor: CoreEditor) => {
|
||||
const prevNodesById: Record<string, Node> = {};
|
||||
previousState.current?.doc.forEach((node) => {
|
||||
if (node.attrs.id) {
|
||||
prevNodesById[node.attrs.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
const nodesById: Record<string, Node> = {};
|
||||
editor.state?.doc.forEach((node) => {
|
||||
if (node.attrs.id) {
|
||||
nodesById[node.attrs.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
previousState.current = editor.state;
|
||||
|
||||
for (const [id, node] of Object.entries(prevNodesById)) {
|
||||
if (nodesById[id] === undefined) {
|
||||
onNodeDeleted(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onNodeDeleted]
|
||||
);
|
||||
|
||||
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
|
||||
setTimeout(async () => {
|
||||
if (onChange) {
|
||||
@ -112,9 +73,8 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
||||
}, 500);
|
||||
}, 1000);
|
||||
|
||||
const editorClassNames = `relative w-full max-w-screen-lg mt-2 p-3 relative focus:outline-none rounded-lg
|
||||
${noBorder ? "" : "border border-custom-border-200"} ${
|
||||
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||
const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
||||
${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||
} ${customClassName}`;
|
||||
|
||||
if (!editor) return null;
|
||||
@ -131,6 +91,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
||||
{editor && <EditorBubbleMenu editor={editor} />}
|
||||
<div className={`${editorContentCustomClassNames}`}>
|
||||
<EditorContent editor={editor} />
|
||||
{editor?.isActive("image") && <ImageResizer editor={editor} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
56
apps/app/components/tiptap/plugins/delete-image.tsx
Normal file
56
apps/app/components/tiptap/plugins/delete-image.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model';
|
||||
import fileService from "services/file.service";
|
||||
|
||||
const deleteKey = new PluginKey("delete-image");
|
||||
|
||||
const TrackImageDeletionPlugin = () =>
|
||||
new Plugin({
|
||||
key: deleteKey,
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
transactions.forEach((transaction) => {
|
||||
if (!transaction.docChanged) return;
|
||||
|
||||
const removedImages: ProseMirrorNode[] = [];
|
||||
|
||||
oldState.doc.descendants((oldNode, oldPos) => {
|
||||
if (oldNode.type.name !== 'image') return;
|
||||
|
||||
if (!newState.doc.resolve(oldPos).parent) return;
|
||||
const newNode = newState.doc.nodeAt(oldPos);
|
||||
|
||||
// Check if the node has been deleted or replaced
|
||||
if (!newNode || newNode.type.name !== 'image') {
|
||||
// Check if the node still exists elsewhere in the document
|
||||
let nodeExists = false;
|
||||
newState.doc.descendants((node) => {
|
||||
if (node.attrs.id === oldNode.attrs.id) {
|
||||
nodeExists = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!nodeExists) {
|
||||
removedImages.push(oldNode as ProseMirrorNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
removedImages.forEach((node) => {
|
||||
const src = node.attrs.src;
|
||||
onNodeDeleted(src);
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export default TrackImageDeletionPlugin;
|
||||
|
||||
async function onNodeDeleted(src: string) {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
||||
if (resStatus === 204) {
|
||||
console.log("Image deleted successfully");
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ function findPlaceholder(state: EditorState, id: {}) {
|
||||
return found.length ? found[0].from : null;
|
||||
}
|
||||
|
||||
export async function startImageUpload(file: File, view: EditorView, pos: number) {
|
||||
export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) {
|
||||
if (!file.type.includes("image/")) {
|
||||
return;
|
||||
} else if (file.size / 1024 / 1024 > 20) {
|
||||
@ -82,7 +82,11 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
|
||||
view.dispatch(tr);
|
||||
};
|
||||
|
||||
const src = await UploadImageHandler(file);
|
||||
if (!workspaceSlug) {
|
||||
return;
|
||||
}
|
||||
setIsSubmitting?.("submitting")
|
||||
const src = await UploadImageHandler(file, workspaceSlug);
|
||||
const { schema } = view.state;
|
||||
pos = findPlaceholder(view.state, id);
|
||||
|
||||
@ -96,7 +100,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
|
||||
view.dispatch(transaction);
|
||||
}
|
||||
|
||||
const UploadImageHandler = (file: File): Promise<string> => {
|
||||
const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string> => {
|
||||
if (!workspaceSlug) {
|
||||
return Promise.reject("Workspace slug is missing");
|
||||
}
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("asset", file);
|
||||
@ -104,7 +111,7 @@ const UploadImageHandler = (file: File): Promise<string> => {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const imageUrl = await fileService
|
||||
.uploadFile("plane", formData)
|
||||
.uploadFile(workspaceSlug, formData)
|
||||
.then((response) => response.asset);
|
||||
|
||||
const image = new Image();
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { startImageUpload } from "./plugins/upload-image";
|
||||
|
||||
export const TiptapEditorProps: EditorProps = {
|
||||
export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
|
||||
return {
|
||||
attributes: {
|
||||
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
|
||||
},
|
||||
@ -25,8 +26,7 @@ export const TiptapEditorProps: EditorProps = {
|
||||
event.preventDefault();
|
||||
const file = event.clipboardData.files[0];
|
||||
const pos = view.state.selection.from;
|
||||
|
||||
startImageUpload(file, view, pos);
|
||||
startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -46,11 +46,11 @@ export const TiptapEditorProps: EditorProps = {
|
||||
});
|
||||
// here we deduct 1 from the pos or else the image will create an extra node
|
||||
if (coordinates) {
|
||||
startImageUpload(file, view, coordinates.pos - 1);
|
||||
startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ const Command = Extension.create({
|
||||
},
|
||||
});
|
||||
|
||||
const getSuggestionItems = ({ query }: { query: string }) =>
|
||||
const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => ({ query }: { query: string }) =>
|
||||
[
|
||||
{
|
||||
title: "Text",
|
||||
@ -163,7 +163,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
||||
if (input.files?.length) {
|
||||
const file = input.files[0];
|
||||
const pos = editor.view.state.selection.from;
|
||||
startImageUpload(file, editor.view, pos);
|
||||
startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
@ -328,11 +328,12 @@ const renderItems = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const SlashCommand = Command.configure({
|
||||
export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
|
||||
Command.configure({
|
||||
suggestion: {
|
||||
items: getSuggestionItems,
|
||||
items: getSuggestionItems(workspaceSlug, setIsSubmitting),
|
||||
render: renderItems,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default SlashCommand;
|
||||
|
@ -52,10 +52,10 @@
|
||||
"highlight.js": "^11.8.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"lowlight": "^2.9.0",
|
||||
"lucide-react": "^0.263.1",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"next": "12.3.2",
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.2.1",
|
||||
@ -68,6 +68,7 @@
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.38.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-moveable": "^0.54.1",
|
||||
"sharp": "^0.32.1",
|
||||
"sonner": "^0.6.2",
|
||||
"swr": "^2.1.3",
|
||||
|
@ -106,6 +106,7 @@ const ProfileActivity = () => {
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<Tiptap
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={
|
||||
activityItem?.new_value !== ""
|
||||
? activityItem.new_value
|
||||
|
@ -126,3 +126,27 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
transition: opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
.img-placeholder {
|
||||
position: relative;
|
||||
width: 35%;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 45%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid rgba(var(--color-text-200));
|
||||
border-top-color: rgba(var(--color-text-800));
|
||||
animation: spinning 0.6s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinning {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
165
yarn.lock
165
yarn.lock
@ -1006,6 +1006,40 @@
|
||||
react-popper "^2.3.0"
|
||||
tslib "~2.5.0"
|
||||
|
||||
"@cfcs/core@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.0.6.tgz#9f8499dcd2ad29fd96d8fa72055411cd4a249121"
|
||||
integrity sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==
|
||||
dependencies:
|
||||
"@egjs/component" "^3.0.2"
|
||||
|
||||
"@daybrush/utils@^1.1.1", "@daybrush/utils@^1.13.0", "@daybrush/utils@^1.4.0", "@daybrush/utils@^1.6.0", "@daybrush/utils@^1.7.1":
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@daybrush/utils/-/utils-1.13.0.tgz#ea70a60864130da476406fdd1d465e3068aea0ff"
|
||||
integrity sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==
|
||||
|
||||
"@egjs/agent@^2.2.1":
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@egjs/agent/-/agent-2.4.3.tgz#6d44e2fb1ff7bab242c07f82732fe60305ac6f06"
|
||||
integrity sha512-XvksSENe8wPeFlEVouvrOhKdx8HMniJ3by7sro2uPF3M6QqWwjzVcmvwoPtdjiX8O1lfRoLhQMp1a7NGlVTdIA==
|
||||
|
||||
"@egjs/children-differ@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@egjs/children-differ/-/children-differ-1.0.1.tgz#5465fa80671d5ca3564ebe912f48b05b3e8a14fd"
|
||||
integrity sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ==
|
||||
dependencies:
|
||||
"@egjs/list-differ" "^1.0.0"
|
||||
|
||||
"@egjs/component@^3.0.2":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.4.tgz#ad7b53794b2a612806179a188ad828acb9525f61"
|
||||
integrity sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g==
|
||||
|
||||
"@egjs/list-differ@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@egjs/list-differ/-/list-differ-1.0.1.tgz#5772b0f8b87973bb67827f6c7d7df8d7f64a22eb"
|
||||
integrity sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg==
|
||||
|
||||
"@emotion/babel-plugin@^11.11.0":
|
||||
version "11.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c"
|
||||
@ -1990,6 +2024,28 @@
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69"
|
||||
integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==
|
||||
|
||||
"@scena/dragscroll@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@scena/dragscroll/-/dragscroll-1.4.0.tgz#220b2430c16119cd3e70044ee533a5b9a43cffd7"
|
||||
integrity sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.6.0"
|
||||
"@scena/event-emitter" "^1.0.2"
|
||||
|
||||
"@scena/event-emitter@^1.0.2", "@scena/event-emitter@^1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@scena/event-emitter/-/event-emitter-1.0.5.tgz#047e3acef93cf238d7ce3a8cc5a12ec6bd9c3bb1"
|
||||
integrity sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.1.1"
|
||||
|
||||
"@scena/matrix@^1.0.0", "@scena/matrix@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@scena/matrix/-/matrix-1.1.1.tgz#5297f71825c72e2c2c8f802f924f482ed200c43c"
|
||||
integrity sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.4.0"
|
||||
|
||||
"@sentry-internal/tracing@7.63.0":
|
||||
version "7.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.63.0.tgz#58903b2205456034611cc5bc1b5b2479275f89c7"
|
||||
@ -3462,6 +3518,21 @@ css-box-model@^1.2.0:
|
||||
dependencies:
|
||||
tiny-invariant "^1.0.6"
|
||||
|
||||
css-styled@^1.0.8, css-styled@~1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/css-styled/-/css-styled-1.0.8.tgz#c9c05dc4abdef5571033090bfb8cfc5e19429974"
|
||||
integrity sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
|
||||
css-to-mat@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/css-to-mat/-/css-to-mat-1.1.1.tgz#0dd10dcf9ec17df15708c8ff07a74fbd0b9a3fe5"
|
||||
integrity sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
"@scena/matrix" "^1.0.0"
|
||||
|
||||
cssesc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
@ -4484,6 +4555,11 @@ fraction.js@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
|
||||
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
|
||||
|
||||
framework-utils@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/framework-utils/-/framework-utils-1.1.0.tgz#a3b528bce838dfd623148847dc92371b09d0da2d"
|
||||
integrity sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg==
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
@ -4539,6 +4615,14 @@ gensync@^1.0.0-beta.2:
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
gesto@^1.19.0, gesto@^1.19.1:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.1.tgz#b2a29730663eecf77b248982bbff929e79d4a461"
|
||||
integrity sha512-ofWVEdqmnpFm3AFf7aoclhoayseb3OkwSiXbXusKYu/99iN5HgeWP+SWqdghQ5TFlOgP5Zlz+6SY8mP2V0kFaQ==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
"@scena/event-emitter" "^1.0.2"
|
||||
|
||||
get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
|
||||
@ -5244,6 +5328,21 @@ jsonpointer@^5.0.0:
|
||||
object.assign "^4.1.4"
|
||||
object.values "^1.1.6"
|
||||
|
||||
keycode@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff"
|
||||
integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==
|
||||
|
||||
keycon@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/keycon/-/keycon-1.4.0.tgz#bf2a633f3c3b659ea564045938cff33e584cebd5"
|
||||
integrity sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A==
|
||||
dependencies:
|
||||
"@cfcs/core" "^0.0.6"
|
||||
"@daybrush/utils" "^1.7.1"
|
||||
"@scena/event-emitter" "^1.0.2"
|
||||
keycode "^2.2.0"
|
||||
|
||||
kleur@^4.0.3:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
||||
@ -6081,6 +6180,13 @@ orderedmap@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
|
||||
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
|
||||
|
||||
overlap-area@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/overlap-area/-/overlap-area-1.1.0.tgz#1fcaa21bdb9cb1ace973d9aa299ae6b56557a4c2"
|
||||
integrity sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.7.1"
|
||||
|
||||
p-limit@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||
@ -6452,20 +6558,13 @@ prosemirror-menu@^1.2.1:
|
||||
prosemirror-history "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1:
|
||||
prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd"
|
||||
integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-model@^1.19.0:
|
||||
version "1.19.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006"
|
||||
integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-schema-basic@^1.2.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7"
|
||||
@ -6603,6 +6702,14 @@ react-color@^2.19.3:
|
||||
reactcss "^1.2.0"
|
||||
tinycolor2 "^1.4.1"
|
||||
|
||||
react-css-styled@^1.1.9:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/react-css-styled/-/react-css-styled-1.1.9.tgz#a7cc948e49f72b2f7fb1393bd85416a8293afab3"
|
||||
integrity sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw==
|
||||
dependencies:
|
||||
css-styled "~1.0.8"
|
||||
framework-utils "^1.1.0"
|
||||
|
||||
react-datepicker@^4.8.0:
|
||||
version "4.16.0"
|
||||
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f"
|
||||
@ -6683,6 +6790,25 @@ react-markdown@^8.0.7:
|
||||
unist-util-visit "^4.0.0"
|
||||
vfile "^5.0.0"
|
||||
|
||||
react-moveable@^0.54.1:
|
||||
version "0.54.1"
|
||||
resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.1.tgz#3c69748c444184700e6999501b0da953c934205e"
|
||||
integrity sha512-Kj2ifw9nk3LZvu7ezhst8Z5WBPRr+yVv9oROwrBirFlHmwGHHZXUGk5Gaezu+JGqqNRsQJncVMW5Uf68KSSOvg==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
"@egjs/agent" "^2.2.1"
|
||||
"@egjs/children-differ" "^1.0.1"
|
||||
"@egjs/list-differ" "^1.0.0"
|
||||
"@scena/dragscroll" "^1.4.0"
|
||||
"@scena/event-emitter" "^1.0.5"
|
||||
"@scena/matrix" "^1.1.1"
|
||||
css-to-mat "^1.1.1"
|
||||
framework-utils "^1.1.0"
|
||||
gesto "^1.19.0"
|
||||
overlap-area "^1.1.0"
|
||||
react-css-styled "^1.1.9"
|
||||
react-selecto "^1.25.0"
|
||||
|
||||
react-onclickoutside@^6.12.2:
|
||||
version "6.13.0"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc"
|
||||
@ -6740,6 +6866,13 @@ react-remove-scroll@2.5.4:
|
||||
use-callback-ref "^1.3.0"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-selecto@^1.25.0:
|
||||
version "1.26.0"
|
||||
resolved "https://registry.yarnpkg.com/react-selecto/-/react-selecto-1.26.0.tgz#9157ff0a732fc426602b30c08ec21b6ca0a9c472"
|
||||
integrity sha512-aBTZEYA68uE+o8TytNjTb2GpIn4oKEv0U4LIow3cspJQlF/PdAnBwkq9UuiKVuFluu5kfLQ7Keu3S2Tihlmw0g==
|
||||
dependencies:
|
||||
selecto "~1.26.0"
|
||||
|
||||
react-style-singleton@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
|
||||
@ -7023,6 +7156,22 @@ schema-utils@^3.1.1:
|
||||
ajv "^6.12.5"
|
||||
ajv-keywords "^3.5.2"
|
||||
|
||||
selecto@~1.26.0:
|
||||
version "1.26.0"
|
||||
resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.0.tgz#f3f04fb6409112b198243458f6c9963946d5ba2f"
|
||||
integrity sha512-cEFKdv5rmkF6pf2OScQJllaNp4UJy/FvviB40ZaMSHrQCxC72X/Q6uhzW1tlb2RE+0danvUNJTs64cI9VXtUyg==
|
||||
dependencies:
|
||||
"@daybrush/utils" "^1.13.0"
|
||||
"@egjs/children-differ" "^1.0.1"
|
||||
"@scena/dragscroll" "^1.4.0"
|
||||
"@scena/event-emitter" "^1.0.5"
|
||||
css-styled "^1.0.8"
|
||||
css-to-mat "^1.1.1"
|
||||
framework-utils "^1.1.0"
|
||||
gesto "^1.19.1"
|
||||
keycon "^1.2.0"
|
||||
overlap-area "^1.1.0"
|
||||
|
||||
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
|
Loading…
Reference in New Issue
Block a user