forked from github/plane
fix: Debounce title and Editor initialization (#2530)
* fixed debounce logic and extracted the same * fixed editor mounting with custom hook * removed console logs and improved structure * fixed comment editor behavior on Shift-Enter * fixed editor initialization behaviour for new peek view * fixed button type to avoid reload while editing comments * fixed initialization of content in peek overview * improved naming variables in updated title debounce logic * added react-hook-form support to the issue detail in peek view with save states * delete image plugin's ts support improved
This commit is contained in:
parent
442c83eea2
commit
8072bbb559
@ -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,
|
||||||
|
} 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";
|
||||||
|
import { useInitializedContent } from "./useInitializedContent";
|
||||||
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,37 @@ 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,
|
||||||
|
deleteFile,
|
||||||
|
editorProps = {},
|
||||||
|
value,
|
||||||
|
extensions = [],
|
||||||
|
onChange,
|
||||||
|
setIsSubmitting,
|
||||||
|
forwardedRef,
|
||||||
|
setShouldShowAlert,
|
||||||
|
}: CustomEditorProps) => {
|
||||||
|
const editor = useCustomEditor(
|
||||||
|
{
|
||||||
editorProps: {
|
editorProps: {
|
||||||
...CoreEditorProps(uploadFile, setIsSubmitting),
|
...CoreEditorProps(uploadFile, setIsSubmitting),
|
||||||
...editorProps,
|
...editorProps,
|
||||||
},
|
},
|
||||||
extensions: [...CoreEditorExtensions(deleteFile), ...extensions],
|
extensions: [...CoreEditorExtensions(deleteFile), ...extensions],
|
||||||
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
|
content:
|
||||||
|
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||||
onUpdate: async ({ editor }) => {
|
onUpdate: async ({ editor }) => {
|
||||||
// for instant feedback loop
|
// for instant feedback loop
|
||||||
setIsSubmitting?.("submitting");
|
setIsSubmitting?.("submitting");
|
||||||
setShouldShowAlert?.(true);
|
setShouldShowAlert?.(true);
|
||||||
if (debouncedUpdatesEnabled) {
|
|
||||||
debouncedUpdates({ onChange: onChange, editor });
|
|
||||||
} else {
|
|
||||||
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useInitializedContent(editor, value);
|
||||||
|
|
||||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
@ -55,12 +72,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;
|
||||||
}
|
}
|
||||||
|
19
packages/editor/core/src/ui/hooks/useInitializedContent.tsx
Normal file
19
packages/editor/core/src/ui/hooks/useInitializedContent.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Editor } from "@tiptap/react";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export const useInitializedContent = (editor: Editor | null, value: string) => {
|
||||||
|
const hasInitializedContent = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor) {
|
||||||
|
const cleanedValue =
|
||||||
|
typeof value === "string" && value.trim() !== "" ? value : "<p></p>";
|
||||||
|
if (cleanedValue !== "<p></p>" && !hasInitializedContent.current) {
|
||||||
|
editor.commands.setContent(cleanedValue);
|
||||||
|
hasInitializedContent.current = true;
|
||||||
|
} else if (cleanedValue === "<p></p>" && hasInitializedContent.current) {
|
||||||
|
hasInitializedContent.current = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value, editor]);
|
||||||
|
};
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
|||||||
new Plugin({
|
new Plugin({
|
||||||
key: deleteKey,
|
key: deleteKey,
|
||||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||||
const newImageSources = new Set();
|
const newImageSources = new Set<string>();
|
||||||
newState.doc.descendants((node) => {
|
newState.doc.descendants((node) => {
|
||||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||||
newImageSources.add(node.attrs.src);
|
newImageSources.add(node.attrs.src);
|
||||||
|
@ -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(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
@ -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(),
|
||||||
|
]),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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),
|
||||||
];
|
];
|
||||||
|
@ -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}
|
||||||
|
@ -116,7 +116,8 @@ export const CommentCard: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 self-end">
|
<div className="flex gap-1 self-end">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="button"
|
||||||
|
onClick={handleSubmit(onEnter)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||||
>
|
>
|
||||||
|
@ -49,6 +49,14 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [localTitleValue, setLocalTitleValue] = useState("");
|
||||||
|
const issueTitleCurrentValue = watch("name");
|
||||||
|
useEffect(() => {
|
||||||
|
if (localTitleValue === "" && issueTitleCurrentValue !== "") {
|
||||||
|
setLocalTitleValue(issueTitleCurrentValue);
|
||||||
|
}
|
||||||
|
}, [issueTitleCurrentValue, localTitleValue]);
|
||||||
|
|
||||||
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={localTitleValue}
|
||||||
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();
|
setLocalTitleValue(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();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC } from "react";
|
import { FC, useCallback, useEffect, useState } from "react";
|
||||||
// packages
|
// packages
|
||||||
import { RichTextEditor } from "@plane/rich-text-editor";
|
import { RichTextEditor } from "@plane/rich-text-editor";
|
||||||
// components
|
// components
|
||||||
@ -9,6 +9,8 @@ import { useDebouncedCallback } from "use-debounce";
|
|||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
// services
|
// services
|
||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
|
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
@ -24,9 +26,47 @@ interface IPeekOverviewIssueDetails {
|
|||||||
|
|
||||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
||||||
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
|
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
|
|
||||||
const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => {
|
const { handleSubmit, watch, reset, control } = useForm<IIssue>({
|
||||||
issueUpdate({ ...issue, description_html: _data });
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
description_html: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!issue) return;
|
||||||
|
|
||||||
|
reset({
|
||||||
|
...issue,
|
||||||
|
});
|
||||||
|
}, [issue, reset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitting === "submitted") {
|
||||||
|
setShowAlert(false);
|
||||||
|
setTimeout(async () => {
|
||||||
|
setIsSubmitting("saved");
|
||||||
|
}, 2000);
|
||||||
|
} else if (isSubmitting === "submitting") {
|
||||||
|
setShowAlert(true);
|
||||||
|
}
|
||||||
|
}, [isSubmitting, setShowAlert]);
|
||||||
|
|
||||||
|
const handleDescriptionFormSubmit = useCallback(
|
||||||
|
async (formData: Partial<IIssue>) => {
|
||||||
|
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
|
||||||
|
|
||||||
|
issueUpdate({ name: formData.name ?? "", description_html: formData.description_html });
|
||||||
|
},
|
||||||
|
[issueUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedIssueFormSave = useDebouncedCallback(async () => {
|
||||||
|
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -35,19 +75,35 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
{issue?.project_detail?.identifier}-{issue?.sequence_id}
|
{issue?.project_detail?.identifier}-{issue?.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="font-medium text-xl">{issue?.name}</div>
|
<div className="font-medium text-xl">{watch("name")}</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Controller
|
||||||
|
name="description_html"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
value={issue?.description_html}
|
value={value}
|
||||||
debouncedUpdatesEnabled={false}
|
|
||||||
onChange={(description: Object, description_html: string) => {
|
onChange={(description: Object, description_html: string) => {
|
||||||
debouncedIssueDescription(description_html);
|
setIsSubmitting("submitting");
|
||||||
|
onChange(description_html);
|
||||||
|
debouncedIssueFormSave();
|
||||||
}}
|
}}
|
||||||
customClassName="p-3 min-h-[80px] shadow-sm"
|
customClassName="p-3 min-h-[80px] shadow-sm"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
issueReactions={issueReactions}
|
issueReactions={issueReactions}
|
||||||
|
Loading…
Reference in New Issue
Block a user