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";
|
||||
// types
|
||||
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
|
||||
import Tiptap from "components/tiptap";
|
||||
// rich-text-editor
|
||||
// const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||
// ssr: false,
|
||||
@ -145,6 +146,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
console.log("values", getValues());
|
||||
|
||||
const issueName = watch("name");
|
||||
|
||||
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||
@ -338,9 +341,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
{issueName && issueName !== "" && (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
|
||||
iAmFeelingLucky ? "cursor-wait" : ""
|
||||
}`}
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${iAmFeelingLucky ? "cursor-wait" : ""
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
>
|
||||
@ -362,23 +364,27 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
AI
|
||||
</button>
|
||||
</div>
|
||||
{/* <Controller */}
|
||||
{/* name="description" */}
|
||||
{/* control={control} */}
|
||||
{/* render={({ field: { value } }) => ( */}
|
||||
{/* <WrappedRemirrorRichTextEditor */}
|
||||
{/* value={ */}
|
||||
{/* !value || (typeof value === "object" && Object.keys(value).length === 0) */}
|
||||
{/* ? watch("description_html") */}
|
||||
{/* : value */}
|
||||
{/* } */}
|
||||
{/* onJSONChange={(jsonValue) => setValue("description", jsonValue)} */}
|
||||
{/* onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} */}
|
||||
{/* placeholder="Description" */}
|
||||
{/* ref={editorRef} */}
|
||||
{/* /> */}
|
||||
{/* )} */}
|
||||
{/* /> */}
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
if (!value && !watch("description_html")) return <></>;
|
||||
|
||||
return (
|
||||
<Tiptap
|
||||
value={
|
||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("description_html")
|
||||
: value
|
||||
}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
setValue("description", description);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<GptAssistantModal
|
||||
isOpen={gptAssistantModal}
|
||||
handleClose={() => {
|
||||
@ -523,7 +529,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
>
|
||||
<span className="text-xs">Create more</span>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||
<ToggleSwitch value={createMore} onChange={() => { }} size="md" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
|
||||
@ -533,8 +539,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
? "Updating Issue..."
|
||||
: "Update Issue"
|
||||
: isSubmitting
|
||||
? "Adding Issue..."
|
||||
: "Add Issue"}
|
||||
? "Adding Issue..."
|
||||
: "Add Issue"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,7 +54,7 @@ export const TiptapExtensions = [
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
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",
|
||||
},
|
||||
},
|
||||
@ -62,7 +62,7 @@ export const TiptapExtensions = [
|
||||
horizontalRule: false,
|
||||
dropcursor: {
|
||||
color: "#DBEAFE",
|
||||
width: 4,
|
||||
width: 2,
|
||||
},
|
||||
gapcursor: false,
|
||||
}),
|
||||
@ -117,7 +117,7 @@ export const TiptapExtensions = [
|
||||
includeChildren: true,
|
||||
}),
|
||||
UniqueID.configure({
|
||||
types: ['heading', 'paragraph', 'image'],
|
||||
types: ['image'],
|
||||
}),
|
||||
SlashCommand,
|
||||
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;
|
||||
customClassName?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting: (isSubmitting: boolean) => void;
|
||||
setIsSubmitting?: (isSubmitting: boolean) => void;
|
||||
}
|
||||
|
||||
const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, customClassName }: TiptapProps) => {
|
||||
@ -24,7 +24,7 @@ const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, cus
|
||||
extensions: TiptapExtensions,
|
||||
content: value,
|
||||
onUpdate: async ({ editor }) => {
|
||||
setIsSubmitting(true);
|
||||
setIsSubmitting?.(true);
|
||||
checkForNodeDeletions(editor)
|
||||
debouncedUpdates({ onChange, editor });
|
||||
}
|
||||
@ -32,13 +32,6 @@ const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, cus
|
||||
|
||||
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(
|
||||
async (node: Node) => {
|
||||
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}`}
|
||||
>
|
||||
{editor && <EditorBubbleMenu editor={editor} />}
|
||||
<div className="pt-8">
|
||||
<div className="pt-9">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user