Compare commits

...

7 Commits

Author SHA1 Message Date
Palanikannan1437
6a96278cd5 added styles for embedding 2023-10-30 10:00:31 +05:30
Palanikannan1437
a4171552e1 initialized loom embedding support 2023-10-30 10:00:10 +05:30
Palanikannan1437
ce18387600 fixed editor initialization behaviour for new peek view 2023-10-30 09:58:07 +05:30
Palanikannan1437
d8cd0b0f97 fixed comment editor behavior on Shift-Enter 2023-10-28 13:08:36 +05:30
Palanikannan1437
2ff953896d removed console logs and improved structure 2023-10-26 14:48:39 +05:30
Palanikannan1437
d714079c1c fixed editor mounting with custom hook 2023-10-24 19:53:43 +05:30
Palanikannan1437
5136f3b329 fixed debounce logic and extracted the same 2023-10-24 19:52:50 +05:30
11 changed files with 386 additions and 211 deletions

View File

@ -1,18 +1,23 @@
import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import { useImperativeHandle, useRef, MutableRefObject } from "react"; import {
import { useDebouncedCallback } from "use-debounce"; useImperativeHandle,
import { DeleteImage } from '../../types/delete-image'; useRef,
MutableRefObject,
useEffect,
useState,
} from "react";
import { DeleteImage } from "../../types/delete-image";
import { CoreEditorProps } from "../props"; import { CoreEditorProps } from "../props";
import { CoreEditorExtensions } from "../extensions"; import { CoreEditorExtensions } from "../extensions";
import { EditorProps } from '@tiptap/pm/view'; import { EditorProps } from "@tiptap/pm/view";
import { getTrimmedHTML } from "../../lib/utils"; import { getTrimmedHTML } from "../../lib/utils";
import { UploadImage } from "../../types/upload-image"; import { UploadImage } from "../../types/upload-image";
const DEBOUNCE_DELAY = 1500;
interface CustomEditorProps { interface CustomEditorProps {
uploadFile: UploadImage; uploadFile: UploadImage;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
value: string; value: string;
deleteFile: DeleteImage; deleteFile: DeleteImage;
@ -23,25 +28,51 @@ interface CustomEditorProps {
forwardedRef?: any; forwardedRef?: any;
} }
export const useEditor = ({ uploadFile, deleteFile, editorProps = {}, value, extensions = [], onChange, setIsSubmitting, debouncedUpdatesEnabled, forwardedRef, setShouldShowAlert, }: CustomEditorProps) => { export const useEditor = ({
const editor = useCustomEditor({ uploadFile,
editorProps: { deleteFile,
...CoreEditorProps(uploadFile, setIsSubmitting), editorProps = {},
...editorProps, value,
}, extensions = [],
extensions: [...CoreEditorExtensions(deleteFile), ...extensions], onChange,
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>", setIsSubmitting,
onUpdate: async ({ editor }) => { forwardedRef,
// for instant feedback loop setShouldShowAlert,
setIsSubmitting?.("submitting"); }: CustomEditorProps) => {
setShouldShowAlert?.(true); const [internalEditorContent, setInternalEditorContent] = useState(value);
if (debouncedUpdatesEnabled) { const editor = useCustomEditor(
debouncedUpdates({ onChange: onChange, editor }); {
} else { editorProps: {
...CoreEditorProps(uploadFile, setIsSubmitting),
...editorProps,
},
extensions: [...CoreEditorExtensions(deleteFile), ...extensions],
content:
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.("submitting");
setShouldShowAlert?.(true);
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
} },
}, },
}); [internalEditorContent],
);
const hasIntiliazedContent = useRef(false);
useEffect(() => {
if (editor) {
const cleanedValue =
typeof value === "string" && value.trim() !== "" ? value : "<p></p>";
if (cleanedValue !== "<p></p>" && !hasIntiliazedContent.current) {
setInternalEditorContent(cleanedValue);
hasIntiliazedContent.current = true;
} else if (cleanedValue === "<p></p>" && hasIntiliazedContent.current) {
hasIntiliazedContent.current = false;
}
}
}, [value, editor]);
const editorRef: MutableRefObject<Editor | null> = useRef(null); const editorRef: MutableRefObject<Editor | null> = useRef(null);
editorRef.current = editor; editorRef.current = editor;
@ -55,12 +86,6 @@ export const useEditor = ({ uploadFile, deleteFile, editorProps = {}, value, ext
}, },
})); }));
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
if (onChange) {
onChange(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
}
}, DEBOUNCE_DELAY);
if (!editor) { if (!editor) {
return null; return null;
} }

View File

@ -1,8 +1,13 @@
import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import { useImperativeHandle, useRef, MutableRefObject } from "react"; import {
useImperativeHandle,
useRef,
MutableRefObject,
useEffect,
} from "react";
import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions"; import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions";
import { CoreReadOnlyEditorProps } from "../../ui/read-only/props"; import { CoreReadOnlyEditorProps } from "../../ui/read-only/props";
import { EditorProps } from '@tiptap/pm/view'; import { EditorProps } from "@tiptap/pm/view";
interface CustomReadOnlyEditorProps { interface CustomReadOnlyEditorProps {
value: string; value: string;
@ -11,10 +16,16 @@ interface CustomReadOnlyEditorProps {
editorProps?: EditorProps; editorProps?: EditorProps;
} }
export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editorProps = {} }: CustomReadOnlyEditorProps) => { export const useReadOnlyEditor = ({
value,
forwardedRef,
extensions = [],
editorProps = {},
}: CustomReadOnlyEditorProps) => {
const editor = useCustomEditor({ const editor = useCustomEditor({
editable: false, editable: false,
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>", content:
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
editorProps: { editorProps: {
...CoreReadOnlyEditorProps, ...CoreReadOnlyEditorProps,
...editorProps, ...editorProps,
@ -22,6 +33,14 @@ export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editor
extensions: [...CoreReadOnlyEditorExtensions, ...extensions], extensions: [...CoreReadOnlyEditorExtensions, ...extensions],
}); });
const hasIntiliazedContent = useRef(false);
useEffect(() => {
if (editor && !value && !hasIntiliazedContent.current) {
editor.commands.setContent(value);
hasIntiliazedContent.current = true;
}
}, [value]);
const editorRef: MutableRefObject<Editor | null> = useRef(null); const editorRef: MutableRefObject<Editor | null> = useRef(null);
editorRef.current = editor; editorRef.current = editor;
@ -34,7 +53,6 @@ export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editor
}, },
})); }));
if (!editor) { if (!editor) {
return null; return null;
} }

View File

@ -1,9 +0,0 @@
import ListItem from '@tiptap/extension-list-item'
export const CustomListItem = ListItem.extend({
addKeyboardShortcuts() {
return {
'Shift-Enter': () => this.editor.chain().focus().splitListItem('listItem').run(),
}
},
})

View File

@ -1,16 +1,25 @@
import { Extension } from '@tiptap/core'; import { Extension } from "@tiptap/core";
export const EnterKeyExtension = (onEnterKeyPress?: () => void) => Extension.create({ export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
name: 'enterKey', Extension.create({
name: "enterKey",
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
'Enter': () => { Enter: () => {
if (onEnterKeyPress) { if (onEnterKeyPress) {
onEnterKeyPress(); onEnterKeyPress();
} }
return true; return true;
}, },
} "Shift-Enter": ({ editor }) =>
}, editor.commands.first(({ commands }) => [
}); () => commands.newlineInCode(),
() => commands.splitListItem("listItem"),
() => commands.createParagraphNear(),
() => commands.liftEmptyBlock(),
() => commands.splitBlock(),
]),
};
},
});

View File

@ -1,7 +1,5 @@
import { CustomListItem } from "./custom-list-extension";
import { EnterKeyExtension } from "./enter-key-extension"; import { EnterKeyExtension } from "./enter-key-extension";
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [ export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [
CustomListItem,
EnterKeyExtension(onEnterKeyPress), EnterKeyExtension(onEnterKeyPress),
]; ];

View File

@ -0,0 +1,76 @@
import { Node } from '@tiptap/core'
export interface IframeOptions {
allowFullscreen: boolean,
HTMLAttributes: {
[key: string]: any
},
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
iframe: {
/**
* Add an iframe
*/
setIframe: (options: { src: string }) => ReturnType,
}
}
}
export const EmbedExtension = Node.create<IframeOptions>({
name: 'iframe',
group: 'block',
atom: true,
addOptions() {
return {
allowFullscreen: true,
HTMLAttributes: {
class: 'iframe-wrapper',
},
}
},
addAttributes() {
return {
src: {
default: null,
},
frameborder: {
default: 0,
},
allowfullscreen: {
default: this.options.allowFullscreen,
parseHTML: () => this.options.allowFullscreen,
},
}
},
parseHTML() {
return [{
tag: 'iframe',
}]
},
renderHTML({ HTMLAttributes }) {
return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]]
},
addCommands() {
return {
setIframe: (options: { src: string }) => ({ tr, dispatch }) => {
const { selection } = tr
const node = this.type.create(options)
if (dispatch) {
tr.replaceRangeWith(selection.from, selection.to, node)
}
return true
},
}
},
})

View File

@ -8,6 +8,7 @@ import ts from "highlight.js/lib/languages/typescript";
import SlashCommand from "./slash-command"; import SlashCommand from "./slash-command";
import { UploadImage } from "../"; import { UploadImage } from "../";
import { EmbedExtension } from "./embed";
const lowlight = createLowlight(common) const lowlight = createLowlight(common)
lowlight.register("ts", ts); lowlight.register("ts", ts);
@ -39,6 +40,7 @@ export const RichTextEditorExtensions = (
class: "mb-6 border-t border-custom-border-300", class: "mb-6 border-t border-custom-border-300",
}, },
}), }),
EmbedExtension,
SlashCommand(uploadFile, setIsSubmitting), SlashCommand(uploadFile, setIsSubmitting),
CodeBlockLowlight.configure({ CodeBlockLowlight.configure({
lowlight, lowlight,

View File

@ -1,4 +1,11 @@
import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react"; import {
useState,
useEffect,
useCallback,
ReactNode,
useRef,
useLayoutEffect,
} from "react";
import { Editor, Range, Extension } from "@tiptap/core"; import { Editor, Range, Extension } from "@tiptap/core";
import Suggestion from "@tiptap/suggestion"; import Suggestion from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react"; import { ReactRenderer } from "@tiptap/react";
@ -18,7 +25,18 @@ import {
Table, Table,
} from "lucide-react"; } from "lucide-react";
import { UploadImage } from "../"; import { UploadImage } from "../";
import { cn, insertTableCommand, toggleBlockquote, toggleBulletList, toggleOrderedList, toggleTaskList, insertImageCommand, toggleHeadingOne, toggleHeadingTwo, toggleHeadingThree } from "@plane/editor-core"; import {
cn,
insertTableCommand,
toggleBlockquote,
toggleBulletList,
toggleOrderedList,
toggleTaskList,
insertImageCommand,
toggleHeadingOne,
toggleHeadingTwo,
toggleHeadingThree,
} from "@plane/editor-core";
interface CommandItemProps { interface CommandItemProps {
title: string; title: string;
@ -37,7 +55,15 @@ const Command = Extension.create({
return { return {
suggestion: { suggestion: {
char: "/", char: "/",
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { command: ({
editor,
range,
props,
}: {
editor: Editor;
range: Range;
props: any;
}) => {
props.command({ editor, range }); props.command({ editor, range });
}, },
}, },
@ -59,127 +85,153 @@ const Command = Extension.create({
const getSuggestionItems = const getSuggestionItems =
( (
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void,
) => ) =>
({ query }: { query: string }) => ({ query }: { query: string }) =>
[ [
{ {
title: "Text", title: "Text",
description: "Just start typing with plain text.", description: "Just start typing with plain text.",
searchTerms: ["p", "paragraph"], searchTerms: ["p", "paragraph"],
icon: <Text size={18} />, icon: <Text size={18} />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); editor
}, .chain()
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.run();
}, },
{ },
title: "Heading 1", {
description: "Big section heading.", title: "Heading 1",
searchTerms: ["title", "big", "large"], description: "Big section heading.",
icon: <Heading1 size={18} />, searchTerms: ["title", "big", "large"],
command: ({ editor, range }: CommandProps) => { icon: <Heading1 size={18} />,
toggleHeadingOne(editor, range); command: ({ editor, range }: CommandProps) => {
}, toggleHeadingOne(editor, range);
}, },
{ },
title: "Heading 2", {
description: "Medium section heading.", title: "Heading 2",
searchTerms: ["subtitle", "medium"], description: "Medium section heading.",
icon: <Heading2 size={18} />, searchTerms: ["subtitle", "medium"],
command: ({ editor, range }: CommandProps) => { icon: <Heading2 size={18} />,
toggleHeadingTwo(editor, range); command: ({ editor, range }: CommandProps) => {
}, toggleHeadingTwo(editor, range);
}, },
{ },
title: "Heading 3", {
description: "Small section heading.", title: "Heading 3",
searchTerms: ["subtitle", "small"], description: "Small section heading.",
icon: <Heading3 size={18} />, searchTerms: ["subtitle", "small"],
command: ({ editor, range }: CommandProps) => { icon: <Heading3 size={18} />,
toggleHeadingThree(editor, range); command: ({ editor, range }: CommandProps) => {
}, toggleHeadingThree(editor, range);
}, },
{ },
title: "To-do List", {
description: "Track tasks with a to-do list.", title: "To-do List",
searchTerms: ["todo", "task", "list", "check", "checkbox"], description: "Track tasks with a to-do list.",
icon: <CheckSquare size={18} />, searchTerms: ["todo", "task", "list", "check", "checkbox"],
command: ({ editor, range }: CommandProps) => { icon: <CheckSquare size={18} />,
toggleTaskList(editor, range) command: ({ editor, range }: CommandProps) => {
}, toggleTaskList(editor, range);
}, },
{ },
title: "Bullet List", {
description: "Create a simple bullet list.", title: "Bullet List",
searchTerms: ["unordered", "point"], description: "Create a simple bullet list.",
icon: <List size={18} />, searchTerms: ["unordered", "point"],
command: ({ editor, range }: CommandProps) => { icon: <List size={18} />,
toggleBulletList(editor, range); command: ({ editor, range }: CommandProps) => {
}, toggleBulletList(editor, range);
}, },
{ },
title: "Divider", {
description: "Visually divide blocks", title: "Divider",
searchTerms: ["line", "divider", "horizontal", "rule", "separate"], description: "Visually divide blocks",
icon: <MinusSquare size={18} />, searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
command: ({ editor, range }: CommandProps) => { icon: <MinusSquare size={18} />,
editor.chain().focus().deleteRange(range).setHorizontalRule().run(); command: ({ editor, range }: CommandProps) => {
}, editor.chain().focus().deleteRange(range).setHorizontalRule().run();
}, },
{ },
title: "Table", {
description: "Create a Table", title: "Table",
searchTerms: ["table", "cell", "db", "data", "tabular"], description: "Create a Table",
icon: <Table size={18} />, searchTerms: ["table", "cell", "db", "data", "tabular"],
command: ({ editor, range }: CommandProps) => { icon: <Table size={18} />,
insertTableCommand(editor, range); command: ({ editor, range }: CommandProps) => {
}, insertTableCommand(editor, range);
}, },
{ },
title: "Numbered List", {
description: "Create a list with numbering.", title: "Numbered List",
searchTerms: ["ordered"], description: "Create a list with numbering.",
icon: <ListOrdered size={18} />, searchTerms: ["ordered"],
command: ({ editor, range }: CommandProps) => { icon: <ListOrdered size={18} />,
toggleOrderedList(editor, range) command: ({ editor, range }: CommandProps) => {
}, toggleOrderedList(editor, range);
}, },
{ },
title: "Quote", {
description: "Capture a quote.", title: "Quote",
searchTerms: ["blockquote"], description: "Capture a quote.",
icon: <TextQuote size={18} />, searchTerms: ["blockquote"],
command: ({ editor, range }: CommandProps) => icon: <TextQuote size={18} />,
toggleBlockquote(editor, range) command: ({ editor, range }: CommandProps) =>
toggleBlockquote(editor, range),
},
{
title: "Code",
description: "Capture a code snippet.",
searchTerms: ["codeblock"],
icon: <Code size={18} />,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon size={18} />,
command: ({ editor, range }: CommandProps) => {
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
}, },
{ },
title: "Code", {
description: "Capture a code snippet.", title: "Iframe",
searchTerms: ["codeblock"], description: "Embed an iframe.",
icon: <Code size={18} />, searchTerms: ["youtube", "loom", "embed"],
command: ({ editor, range }: CommandProps) => icon: <ImageIcon size={18} />,
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), command: ({ editor, range }: CommandProps) => {
const url = window.prompt("URL");
if (url) {
console.log(url);
editor
.chain()
.focus()
.deleteRange(range)
.setIframe({ src: url })
.run();
}
}, },
{ },
title: "Image", ].filter((item) => {
description: "Upload an image from your computer.", if (typeof query === "string" && query.length > 0) {
searchTerms: ["photo", "picture", "media"], const search = query.toLowerCase();
icon: <ImageIcon size={18} />, return (
command: ({ editor, range }: CommandProps) => { item.title.toLowerCase().includes(search) ||
insertImageCommand(editor, uploadFile, setIsSubmitting, range); item.description.toLowerCase().includes(search) ||
}, (item.searchTerms &&
}, item.searchTerms.some((term: string) => term.includes(search)))
].filter((item) => { );
if (typeof query === "string" && query.length > 0) { }
const search = query.toLowerCase(); return true;
return ( });
item.title.toLowerCase().includes(search) ||
item.description.toLowerCase().includes(search) ||
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
);
}
return true;
});
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight; const containerHeight = container.offsetHeight;
@ -213,7 +265,7 @@ const CommandList = ({
command(item); command(item);
} }
}, },
[command, items] [command, items],
); );
useEffect(() => { useEffect(() => {
@ -266,7 +318,10 @@ const CommandList = ({
<button <button
className={cn( className={cn(
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`, `flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
{ "bg-custom-primary-100/5 text-custom-text-100": index === selectedIndex } {
"bg-custom-primary-100/5 text-custom-text-100":
index === selectedIndex,
},
)} )}
key={index} key={index}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
@ -331,7 +386,9 @@ const renderItems = () => {
export const SlashCommand = ( export const SlashCommand = (
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void,
) => ) =>
Command.configure({ Command.configure({
suggestion: { suggestion: {

View File

@ -7,7 +7,7 @@ import { FileService } from "services/file.service";
// components // components
import { LiteTextEditorWithRef } from "@plane/lite-text-editor"; import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
// ui // ui
import { Button, Tooltip } from "@plane/ui"; import { Button } from "@plane/ui";
import { Globe2, Lock } from "lucide-react"; import { Globe2, Lock } from "lucide-react";
// types // types
@ -72,35 +72,6 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAc
<form onSubmit={handleSubmit(handleAddComment)}> <form onSubmit={handleSubmit(handleAddComment)}>
<div> <div>
<div className="relative"> <div className="relative">
{showAccessSpecifier && (
<div className="absolute bottom-2 left-3 z-[1]">
<Controller
control={control}
name="access"
render={({ field: { onChange, value } }) => (
<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
{commentAccess.map((access) => (
<Tooltip key={access.key} tooltipContent={access.label}>
<button
type="button"
onClick={() => onChange(access.key)}
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${
value === access.key ? "bg-custom-background-80" : ""
}`}
>
<access.icon
className={`w-4 h-4 -mt-1 ${
value === access.key ? "!text-custom-text-100" : "!text-custom-text-400"
}`}
/>
</button>
</Tooltip>
))}
</div>
)}
/>
</div>
)}
<Controller <Controller
name="access" name="access"
control={control} control={control}

View File

@ -49,6 +49,14 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
}, },
}); });
const [localValue, setLocalValue] = useState("");
const nameValue = watch("name");
useEffect(() => {
if (localValue === "" && nameValue !== "") {
setLocalValue(nameValue);
}
}, [nameValue, localValue]);
const handleDescriptionFormSubmit = useCallback( const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<IIssue>) => { async (formData: Partial<IIssue>) => {
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
@ -81,7 +89,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
}); });
}, [issue, reset]); }, [issue, reset]);
const debouncedTitleSave = useDebouncedCallback(async () => { const debouncedFormSave = useDebouncedCallback(async () => {
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted")); handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
}, 1500); }, 1500);
@ -92,18 +100,19 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
<Controller <Controller
name="name" name="name"
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={({ field: { onChange } }) => (
<TextArea <TextArea
value={localValue}
id="name" id="name"
name="name" name="name"
value={value}
placeholder="Enter issue name" placeholder="Enter issue name"
onFocus={() => setCharacterLimit(true)} onFocus={() => setCharacterLimit(true)}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
setCharacterLimit(false); setCharacterLimit(false);
setIsSubmitting("submitting"); setIsSubmitting("submitting");
debouncedTitleSave(); setLocalValue(e.target.value);
onChange(e.target.value); onChange(e.target.value);
debouncedFormSave();
}} }}
required={true} required={true}
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary" className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
@ -135,7 +144,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
uploadFile={fileService.getUploadFileFunction(workspaceSlug)} uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage} deleteFile={fileService.deleteImage}
value={value} value={value}
debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert} setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"} customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
@ -144,7 +152,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
setShowAlert(true); setShowAlert(true);
setIsSubmitting("submitting"); setIsSubmitting("submitting");
onChange(description_html); onChange(description_html);
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted")); debouncedFormSave();
}} }}
/> />
)} )}

View File

@ -229,3 +229,23 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
.ProseMirror table * .is-empty::before { .ProseMirror table * .is-empty::before {
opacity: 0; opacity: 0;
} }
/* iframe */
.iframe-wrapper {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
width: 100%;
height: auto;
}
.iframe-wrapper.ProseMirror-selectednode {
outline: 3px solid #68cef8;
}
.iframe-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}