[WEB-438] fix: ai insertion behaviour (#3872)

* fixed ai insertion behaviour

* replaced all ai popover references to have similar behavior

* chore: removed debug statements
This commit is contained in:
M. Palanikannan 2024-03-06 20:38:57 +05:30 committed by GitHub
parent cb5198c883
commit 549f6d0943
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 45 additions and 21 deletions

View File

@ -53,7 +53,7 @@
"react-moveable": "^0.54.2", "react-moveable": "^0.54.2",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.2" "tiptap-markdown": "^0.8.9"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.15.3", "@types/node": "18.15.3",

View File

@ -0,0 +1,17 @@
import { Selection } from "@tiptap/pm/state";
import { Editor } from "@tiptap/react";
import { MutableRefObject } from "react";
export const insertContentAtSavedSelection = (
editorRef: MutableRefObject<Editor | null>,
content: string,
savedSelection: Selection
) => {
if (editorRef.current && savedSelection) {
editorRef.current
.chain()
.focus()
.insertContentAt(savedSelection?.anchor, content)
.run();
}
};

View File

@ -1,5 +1,5 @@
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, useState } from "react";
import { CoreEditorProps } from "src/ui/props"; import { CoreEditorProps } from "src/ui/props";
import { CoreEditorExtensions } from "src/ui/extensions"; import { CoreEditorExtensions } from "src/ui/extensions";
import { EditorProps } from "@tiptap/pm/view"; import { EditorProps } from "@tiptap/pm/view";
@ -8,6 +8,8 @@ import { DeleteImage } from "src/types/delete-image";
import { IMentionSuggestion } from "src/types/mention-suggestion"; import { IMentionSuggestion } from "src/types/mention-suggestion";
import { RestoreImage } from "src/types/restore-image"; import { RestoreImage } from "src/types/restore-image";
import { UploadImage } from "src/types/upload-image"; import { UploadImage } from "src/types/upload-image";
import { Selection } from "@tiptap/pm/state";
import { insertContentAtSavedSelection } from "src/helpers/insert-content-at-cursor-position";
interface CustomEditorProps { interface CustomEditorProps {
uploadFile: UploadImage; uploadFile: UploadImage;
@ -70,8 +72,10 @@ export const useEditor = ({
onCreate: async ({ editor }) => { onCreate: async ({ editor }) => {
onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
}, },
onTransaction: async ({ editor }) => {
setSavedSelection(editor.state.selection);
},
onUpdate: async ({ editor }) => { onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.("submitting"); setIsSubmitting?.("submitting");
setShouldShowAlert?.(true); setShouldShowAlert?.(true);
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
@ -83,6 +87,8 @@ export const useEditor = ({
const editorRef: MutableRefObject<Editor | null> = useRef(null); const editorRef: MutableRefObject<Editor | null> = useRef(null);
editorRef.current = editor; editorRef.current = editor;
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
useImperativeHandle(forwardedRef, () => ({ useImperativeHandle(forwardedRef, () => ({
clearEditor: () => { clearEditor: () => {
editorRef.current?.commands.clearContent(); editorRef.current?.commands.clearContent();
@ -90,6 +96,11 @@ export const useEditor = ({
setEditorValue: (content: string) => { setEditorValue: (content: string) => {
editorRef.current?.commands.setContent(content); editorRef.current?.commands.setContent(content);
}, },
setEditorValueAtCursorPosition: (content: string) => {
if (savedSelection) {
insertContentAtSavedSelection(editorRef, content, savedSelection);
}
},
})); }));
if (!editor) { if (!editor) {

View File

@ -55,6 +55,7 @@ interface DocumentEditorProps extends IDocumentEditor {
interface EditorHandle { interface EditorHandle {
clearEditor: () => void; clearEditor: () => void;
setEditorValue: (content: string) => void; setEditorValue: (content: string) => void;
setEditorValueAtCursorPosition: (content: string) => void;
} }
const DocumentEditor = ({ const DocumentEditor = ({

View File

@ -45,6 +45,7 @@ export interface RichTextEditorProps extends IRichTextEditor {
interface EditorHandle { interface EditorHandle {
clearEditor: () => void; clearEditor: () => void;
setEditorValue: (content: string) => void; setEditorValue: (content: string) => void;
setEditorValueAtCursorPosition: (content: string) => void;
} }
const RichTextEditor = ({ const RichTextEditor = ({

View File

@ -119,9 +119,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
const handleAiAssistance = async (response: string) => { const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
// setValue("description", {}); editorRef.current?.setEditorValueAtCursorPosition(response);
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
editorRef.current?.setEditorValue(`${watch("description_html")}`);
}; };
const handleAutoGenerateDescription = async () => { const handleAutoGenerateDescription = async () => {

View File

@ -180,8 +180,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const handleAiAssistance = async (response: string) => { const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
setValue("description_html", `${watch("description_html")}<p>${response}</p>`); editorRef.current?.setEditorValueAtCursorPosition(response);
editorRef.current?.setEditorValue(`${watch("description_html")}`);
}; };
const handleAutoGenerateDescription = async () => { const handleAutoGenerateDescription = async () => {

View File

@ -53,7 +53,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { handleSubmit, setValue, watch, getValues, control, reset } = useForm<IPage>({ const { handleSubmit, getValues, control, reset } = useForm<IPage>({
defaultValues: { name: "", description_html: "" }, defaultValues: { name: "", description_html: "" },
}); });
@ -124,16 +124,13 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
const updatePage = async (formData: IPage) => { const updatePage = async (formData: IPage) => {
if (!workspaceSlug || !projectId || !pageId) return; if (!workspaceSlug || !projectId || !pageId) return;
await updateDescriptionAction(formData.description_html); updateDescriptionAction(formData.description_html);
}; };
const handleAiAssistance = async (response: string) => { const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId || !pageId) return; if (!workspaceSlug || !projectId || !pageId) return;
const newDescription = `${watch("description_html")}<p>${response}</p>`; editorRef.current?.setEditorValueAtCursorPosition(response);
setValue("description_html", newDescription);
editorRef.current?.setEditorValue(newDescription);
updateDescriptionAction(newDescription);
}; };
const actionCompleteAlert = ({ const actionCompleteAlert = ({

View File

@ -35,7 +35,7 @@ export interface IPageStore {
addToFavorites: () => Promise<void>; addToFavorites: () => Promise<void>;
removeFromFavorites: () => Promise<void>; removeFromFavorites: () => Promise<void>;
updateName: (name: string) => Promise<void>; updateName: (name: string) => Promise<void>;
updateDescription: (description: string) => Promise<void>; updateDescription: (description: string) => void;
// Reactions // Reactions
disposers: Array<() => void>; disposers: Array<() => void>;
@ -89,7 +89,7 @@ export class PageStore implements IPageStore {
addToFavorites: action, addToFavorites: action,
removeFromFavorites: action, removeFromFavorites: action,
updateName: action, updateName: action,
updateDescription: action, updateDescription: action.bound,
setIsSubmitting: action, setIsSubmitting: action,
cleanup: action, cleanup: action,
}); });
@ -166,7 +166,7 @@ export class PageStore implements IPageStore {
this.name = name; this.name = name;
}); });
updateDescription = action("updateDescription", async (description_html: string) => { updateDescription = action("updateDescription", (description_html: string) => {
const { projectId, workspaceSlug } = this.rootStore.app.router; const { projectId, workspaceSlug } = this.rootStore.app.router;
if (!projectId || !workspaceSlug) return; if (!projectId || !workspaceSlug) return;

View File

@ -8153,10 +8153,10 @@ tippy.js@^6.3.1, tippy.js@^6.3.7:
dependencies: dependencies:
"@popperjs/core" "^2.9.0" "@popperjs/core" "^2.9.0"
tiptap-markdown@^0.8.2: tiptap-markdown@^0.8.9:
version "0.8.8" version "0.8.9"
resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.8.tgz#1e25f40b726239dff84b99a53eb1bdf4af0a02f9" resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.9.tgz#e13f3ae9a1b1649f8c28bb3cae4516a53da7492c"
integrity sha512-I2w/IpvCZ1BoR3nQzG0wRK3uGmDv+Ohyr++G24Ma6RzoDYd0TVGXZp0BOODX5Jj4c6heVY8eksahSeAwJMZBeg== integrity sha512-TykSDcsb94VFCzPbSSTfB6Kh2HJi7x4B9J3Jm9uSOAMPy8App1YfrLW/rEJLajTxwMVhWBdOo4nidComSlLQsQ==
dependencies: dependencies:
"@types/markdown-it" "^12.2.3" "@types/markdown-it" "^12.2.3"
markdown-it "^13.0.1" markdown-it "^13.0.1"