forked from github/plane
Integrated tiptap with Issue Modal
This commit is contained in:
parent
5228ab8d0a
commit
5c290e1302
@ -36,6 +36,7 @@ import {
|
|||||||
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
|
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
|
||||||
|
import Tiptap from "components/tiptap";
|
||||||
// rich-text-editor
|
// rich-text-editor
|
||||||
// const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
// const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||||
// ssr: false,
|
// ssr: false,
|
||||||
@ -145,6 +146,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("values", getValues());
|
||||||
|
|
||||||
const issueName = watch("name");
|
const issueName = watch("name");
|
||||||
|
|
||||||
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||||
@ -338,9 +341,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
{issueName && issueName !== "" && (
|
{issueName && issueName !== "" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
|
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${iAmFeelingLucky ? "cursor-wait" : ""
|
||||||
iAmFeelingLucky ? "cursor-wait" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={handleAutoGenerateDescription}
|
onClick={handleAutoGenerateDescription}
|
||||||
disabled={iAmFeelingLucky}
|
disabled={iAmFeelingLucky}
|
||||||
>
|
>
|
||||||
@ -362,23 +364,27 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
AI
|
AI
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* <Controller */}
|
<Controller
|
||||||
{/* name="description" */}
|
name="description_html"
|
||||||
{/* control={control} */}
|
control={control}
|
||||||
{/* render={({ field: { value } }) => ( */}
|
render={({ field: { value, onChange } }) => {
|
||||||
{/* <WrappedRemirrorRichTextEditor */}
|
if (!value && !watch("description_html")) return <></>;
|
||||||
{/* value={ */}
|
|
||||||
{/* !value || (typeof value === "object" && Object.keys(value).length === 0) */}
|
return (
|
||||||
{/* ? watch("description_html") */}
|
<Tiptap
|
||||||
{/* : value */}
|
value={
|
||||||
{/* } */}
|
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||||
{/* onJSONChange={(jsonValue) => setValue("description", jsonValue)} */}
|
? watch("description_html")
|
||||||
{/* onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} */}
|
: value
|
||||||
{/* placeholder="Description" */}
|
}
|
||||||
{/* ref={editorRef} */}
|
onChange={(description: Object, description_html: string) => {
|
||||||
{/* /> */}
|
onChange(description_html);
|
||||||
{/* )} */}
|
setValue("description", description);
|
||||||
{/* /> */}
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<GptAssistantModal
|
<GptAssistantModal
|
||||||
isOpen={gptAssistantModal}
|
isOpen={gptAssistantModal}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
@ -523,7 +529,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||||
>
|
>
|
||||||
<span className="text-xs">Create more</span>
|
<span className="text-xs">Create more</span>
|
||||||
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
<ToggleSwitch value={createMore} onChange={() => { }} size="md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
|
||||||
@ -533,8 +539,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
? "Updating Issue..."
|
? "Updating Issue..."
|
||||||
: "Update Issue"
|
: "Update Issue"
|
||||||
: isSubmitting
|
: isSubmitting
|
||||||
? "Adding Issue..."
|
? "Adding Issue..."
|
||||||
: "Add Issue"}
|
: "Add Issue"}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,7 +54,7 @@ export const TiptapExtensions = [
|
|||||||
code: {
|
code: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class:
|
class:
|
||||||
"rounded-md bg-stone-200 px-1 py-1 font-mono font-medium text-stone-900",
|
"rounded-md bg-custom-bg-1000 px-1 py-1 font-mono font-medium text-stone-900",
|
||||||
spellcheck: "false",
|
spellcheck: "false",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -62,7 +62,7 @@ export const TiptapExtensions = [
|
|||||||
horizontalRule: false,
|
horizontalRule: false,
|
||||||
dropcursor: {
|
dropcursor: {
|
||||||
color: "#DBEAFE",
|
color: "#DBEAFE",
|
||||||
width: 4,
|
width: 2,
|
||||||
},
|
},
|
||||||
gapcursor: false,
|
gapcursor: false,
|
||||||
}),
|
}),
|
||||||
@ -117,7 +117,7 @@ export const TiptapExtensions = [
|
|||||||
includeChildren: true,
|
includeChildren: true,
|
||||||
}),
|
}),
|
||||||
UniqueID.configure({
|
UniqueID.configure({
|
||||||
types: ['heading', 'paragraph', 'image'],
|
types: ['image'],
|
||||||
}),
|
}),
|
||||||
SlashCommand,
|
SlashCommand,
|
||||||
TiptapUnderline,
|
TiptapUnderline,
|
||||||
|
18
apps/app/components/tiptap/hooks/useDebouncedUpdates.tsx
Normal file
18
apps/app/components/tiptap/hooks/useDebouncedUpdates.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { Editor as CoreEditor } from "@tiptap/core";
|
||||||
|
|
||||||
|
type DebouncedUpdatesProps = {
|
||||||
|
onChange?: (json: any, html: string) => void;
|
||||||
|
editor: CoreEditor | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDebouncedUpdates = (props: DebouncedUpdatesProps) =>
|
||||||
|
useDebouncedCallback(async () => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(props.editor.getJSON(), props.editor.getHTML());
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}, 1000);
|
||||||
|
;
|
||||||
|
|
51
apps/app/components/tiptap/hooks/useNodeDeletion.tsx
Normal file
51
apps/app/components/tiptap/hooks/useNodeDeletion.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { Node } from "@tiptap/pm/model";
|
||||||
|
import { Editor as CoreEditor } from "@tiptap/core";
|
||||||
|
import { EditorState } from '@tiptap/pm/state';
|
||||||
|
import fileService from 'services/file.service';
|
||||||
|
|
||||||
|
export const useNodeDeletion = () => {
|
||||||
|
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.deleteFile(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],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { checkForNodeDeletions };
|
||||||
|
};
|
@ -15,7 +15,7 @@ type TiptapProps = {
|
|||||||
borderOnFocus?: boolean;
|
borderOnFocus?: boolean;
|
||||||
customClassName?: string;
|
customClassName?: string;
|
||||||
onChange?: (json: any, html: string) => void;
|
onChange?: (json: any, html: string) => void;
|
||||||
setIsSubmitting: (isSubmitting: boolean) => void;
|
setIsSubmitting?: (isSubmitting: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, customClassName }: TiptapProps) => {
|
const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, customClassName }: TiptapProps) => {
|
||||||
@ -24,7 +24,7 @@ const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, cus
|
|||||||
extensions: TiptapExtensions,
|
extensions: TiptapExtensions,
|
||||||
content: value,
|
content: value,
|
||||||
onUpdate: async ({ editor }) => {
|
onUpdate: async ({ editor }) => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting?.(true);
|
||||||
checkForNodeDeletions(editor)
|
checkForNodeDeletions(editor)
|
||||||
debouncedUpdates({ onChange, editor });
|
debouncedUpdates({ onChange, editor });
|
||||||
}
|
}
|
||||||
@ -32,13 +32,6 @@ const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, cus
|
|||||||
|
|
||||||
const previousState = useRef<EditorState>();
|
const previousState = useRef<EditorState>();
|
||||||
|
|
||||||
const extractPath = useCallback((url: string, searchString: string) => {
|
|
||||||
if (url.startsWith(searchString)) {
|
|
||||||
console.log("chala", url, searchString)
|
|
||||||
return url.substring(searchString.length);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onNodeDeleted = useCallback(
|
const onNodeDeleted = useCallback(
|
||||||
async (node: Node) => {
|
async (node: Node) => {
|
||||||
if (node.type.name === 'image') {
|
if (node.type.name === 'image') {
|
||||||
@ -100,7 +93,7 @@ const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, cus
|
|||||||
className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`}
|
className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`}
|
||||||
>
|
>
|
||||||
{editor && <EditorBubbleMenu editor={editor} />}
|
{editor && <EditorBubbleMenu editor={editor} />}
|
||||||
<div className="pt-8">
|
<div className="pt-9">
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user