[WIKI-638] fix: peek overview closing while dropdowns are open (#7841)

This commit is contained in:
Aaryan Khandelwal 2025-09-24 17:43:02 +05:30 committed by GitHub
parent 0ed49a6989
commit dce8b75a1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 78 additions and 23 deletions

View file

@ -29,7 +29,8 @@ import { IssueTitleInput } from "../title-input";
// services init // services init
const workItemVersionService = new WorkItemVersionService(); const workItemVersionService = new WorkItemVersionService();
interface IPeekOverviewIssueDetails { type Props = {
editorRef: React.RefObject<EditorRefApi>;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
issueId: string; issueId: string;
@ -38,12 +39,11 @@ interface IPeekOverviewIssueDetails {
isArchived: boolean; isArchived: boolean;
isSubmitting: TNameDescriptionLoader; isSubmitting: TNameDescriptionLoader;
setIsSubmitting: (value: TNameDescriptionLoader) => void; setIsSubmitting: (value: TNameDescriptionLoader) => void;
} };
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer((props) => { export const PeekOverviewIssueDetails: FC<Props> = observer((props) => {
const { workspaceSlug, issueId, issueOperations, disabled, isArchived, isSubmitting, setIsSubmitting } = props; const { editorRef, workspaceSlug, issueId, issueOperations, disabled, isArchived, isSubmitting, setIsSubmitting } =
// refs props;
const editorRef = useRef<EditorRefApi>(null);
// store hooks // store hooks
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { const {

View file

@ -2,6 +2,7 @@ import { FC, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
// plane imports // plane imports
import type { EditorRefApi } from "@plane/editor";
import { EIssueServiceType, TNameDescriptionLoader } from "@plane/types"; import { EIssueServiceType, TNameDescriptionLoader } from "@plane/types";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// hooks // hooks
@ -53,6 +54,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const [isEditIssueModalOpen, setIsEditIssueModalOpen] = useState(false); const [isEditIssueModalOpen, setIsEditIssueModalOpen] = useState(false);
// ref // ref
const issuePeekOverviewRef = useRef<HTMLDivElement>(null); const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<EditorRefApi>(null);
// store hooks // store hooks
const { const {
setPeekIssue, setPeekIssue,
@ -80,8 +82,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
usePeekOverviewOutsideClickDetector( usePeekOverviewOutsideClickDetector(
issuePeekOverviewRef, issuePeekOverviewRef,
() => { () => {
const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen();
if (!embedIssue) { if (!embedIssue) {
if (!isAnyModalOpen && !isAnyEpicModalOpen && !isAnyLocalModalOpen) { if (!isAnyModalOpen && !isAnyEpicModalOpen && !isAnyLocalModalOpen && !isAnyDropbarOpen) {
removeRoutePeekId(); removeRoutePeekId();
} }
} }
@ -90,10 +93,10 @@ export const IssueView: FC<IIssueView> = observer((props) => {
); );
const handleKeyDown = () => { const handleKeyDown = () => {
const slashCommandDropdownElement = document.querySelector("#slash-command");
const editorImageFullScreenModalElement = document.querySelector(".editor-image-full-screen-modal"); const editorImageFullScreenModalElement = document.querySelector(".editor-image-full-screen-modal");
const dropdownElement = document.activeElement?.tagName === "INPUT"; const dropdownElement = document.activeElement?.tagName === "INPUT";
if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement && !editorImageFullScreenModalElement) { const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen();
if (!isAnyModalOpen && !dropdownElement && !isAnyDropbarOpen && !editorImageFullScreenModalElement) {
removeRoutePeekId(); removeRoutePeekId();
const issueElement = document.getElementById(`issue-${issueId}`); const issueElement = document.getElementById(`issue-${issueId}`);
if (issueElement) issueElement?.focus(); if (issueElement) issueElement?.focus();
@ -166,6 +169,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
{["side-peek", "modal"].includes(peekMode) ? ( {["side-peek", "modal"].includes(peekMode) ? (
<div className="relative flex flex-col gap-3 px-8 py-5 space-y-3"> <div className="relative flex flex-col gap-3 px-8 py-5 space-y-3">
<PeekOverviewIssueDetails <PeekOverviewIssueDetails
editorRef={editorRef}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
@ -206,6 +210,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
<div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5"> <div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5">
<div className="space-y-3"> <div className="space-y-3">
<PeekOverviewIssueDetails <PeekOverviewIssueDetails
editorRef={editorRef}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}

View file

@ -55,11 +55,14 @@ const Command = Extension.create<SlashCommandOptions>({
}, },
}); });
const renderItems = () => { const renderItems: SuggestionOptions["render"] = () => {
let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null; let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
let popup: Instance | null = null; let popup: Instance | null = null;
return { return {
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { onStart: (props) => {
// Track active dropdown
props.editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.SLASH_COMMANDS);
component = new ReactRenderer<CommandListInstance, SlashCommandsMenuProps>(SlashCommandsMenu, { component = new ReactRenderer<CommandListInstance, SlashCommandsMenuProps>(SlashCommandsMenu, {
props, props,
editor: props.editor, editor: props.editor,
@ -78,14 +81,14 @@ const renderItems = () => {
placement: "bottom-start", placement: "bottom-start",
}); });
}, },
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { onUpdate: (props) => {
component?.updateProps(props); component?.updateProps(props);
popup?.[0]?.setProps({ popup?.[0]?.setProps({
getReferenceClientRect: props.clientRect, getReferenceClientRect: props.clientRect,
}); });
}, },
onKeyDown: (props: { event: KeyboardEvent }) => { onKeyDown: (props) => {
if (props.event.key === "Escape") { if (props.event.key === "Escape") {
popup?.[0].hide(); popup?.[0].hide();
return true; return true;
@ -95,7 +98,9 @@ const renderItems = () => {
} }
return false; return false;
}, },
onExit: () => { onExit: ({ editor }) => {
// Remove from active dropdowns
editor?.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.SLASH_COMMANDS);
popup?.[0].destroy(); popup?.[0].destroy();
component?.destroy(); component?.destroy();
}, },

View file

@ -15,6 +15,8 @@ import { Ellipsis } from "lucide-react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
// plane imports // plane imports
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// extensions // extensions
import { import {
findTable, findTable,
@ -59,7 +61,16 @@ export const ColumnDragHandle: React.FC<ColumnDragHandleProps> = (props) => {
}), }),
], ],
open: isDropdownOpen, open: isDropdownOpen,
onOpenChange: setIsDropdownOpen, onOpenChange: (open) => {
setIsDropdownOpen(open);
if (open) {
editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
} else {
setTimeout(() => {
editor.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
}, 0);
}
},
whileElementsMounted: autoUpdate, whileElementsMounted: autoUpdate,
}); });
const click = useClick(context); const click = useClick(context);
@ -185,7 +196,6 @@ export const ColumnDragHandle: React.FC<ColumnDragHandleProps> = (props) => {
}} }}
lockScroll lockScroll
/> />
<div <div
className="max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg" className="max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg"
ref={refs.setFloating} ref={refs.setFloating}
@ -195,7 +205,7 @@ export const ColumnDragHandle: React.FC<ColumnDragHandleProps> = (props) => {
zIndex: 100, zIndex: 100,
}} }}
> >
<ColumnOptionsDropdown editor={editor} onClose={() => setIsDropdownOpen(false)} /> <ColumnOptionsDropdown editor={editor} onClose={() => context.onOpenChange(false)} />
</div> </div>
</FloatingPortal> </FloatingPortal>
)} )}

View file

@ -15,6 +15,8 @@ import { Ellipsis } from "lucide-react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
// plane imports // plane imports
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// extensions // extensions
import { import {
findTable, findTable,
@ -59,7 +61,16 @@ export const RowDragHandle: React.FC<RowDragHandleProps> = (props) => {
}), }),
], ],
open: isDropdownOpen, open: isDropdownOpen,
onOpenChange: setIsDropdownOpen, onOpenChange: (open) => {
setIsDropdownOpen(open);
if (open) {
editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
} else {
setTimeout(() => {
editor.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
}, 0);
}
},
whileElementsMounted: autoUpdate, whileElementsMounted: autoUpdate,
}); });
const click = useClick(context); const click = useClick(context);
@ -184,7 +195,6 @@ export const RowDragHandle: React.FC<RowDragHandleProps> = (props) => {
}} }}
lockScroll lockScroll
/> />
<div <div
className="max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg" className="max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg"
ref={refs.setFloating} ref={refs.setFloating}
@ -194,7 +204,7 @@ export const RowDragHandle: React.FC<RowDragHandleProps> = (props) => {
zIndex: 100, zIndex: 100,
}} }}
> >
<RowOptionsDropdown editor={editor} onClose={() => setIsDropdownOpen(false)} /> <RowOptionsDropdown editor={editor} onClose={() => context.onOpenChange(false)} />
</div> </div>
</FloatingPortal> </FloatingPortal>
)} )}

View file

@ -9,13 +9,18 @@ import { DropHandlerPlugin } from "@/plugins/drop";
import { FilePlugins } from "@/plugins/file/root"; import { FilePlugins } from "@/plugins/file/root";
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard"; import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
// types // types
import type { IEditorProps, TEditorAsset, TFileHandler } from "@/types"; import type { IEditorProps, TEditorAsset, TFileHandler } from "@/types";
type TActiveDropbarExtensions = CORE_EXTENSIONS.MENTION | CORE_EXTENSIONS.EMOJI | TAdditionalActiveDropbarExtensions;
type TActiveDropbarExtensions =
| CORE_EXTENSIONS.MENTION
| CORE_EXTENSIONS.EMOJI
| CORE_EXTENSIONS.SLASH_COMMANDS
| CORE_EXTENSIONS.TABLE
| TAdditionalActiveDropbarExtensions;
declare module "@tiptap/core" { declare module "@tiptap/core" {
interface Commands { interface Commands {
utility: { [CORE_EXTENSIONS.UTILITY]: {
updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void; updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
updateAssetsList: ( updateAssetsList: (
args: args:
@ -26,6 +31,8 @@ declare module "@tiptap/core" {
idToRemove: string; idToRemove: string;
} }
) => () => void; ) => () => void;
addActiveDropbarExtension: (extension: TActiveDropbarExtensions) => () => void;
removeActiveDropbarExtension: (extension: TActiveDropbarExtensions) => () => void;
}; };
} }
} }
@ -102,6 +109,18 @@ export const UtilityExtension = (props: Props) => {
} }
this.storage.assetsList = Array.from(uniqueAssets); this.storage.assetsList = Array.from(uniqueAssets);
}, },
addActiveDropbarExtension: (extension) => () => {
const index = this.storage.activeDropbarExtensions.indexOf(extension);
if (index === -1) {
this.storage.activeDropbarExtensions.push(extension);
}
},
removeActiveDropbarExtension: (extension) => () => {
const index = this.storage.activeDropbarExtensions.indexOf(extension);
if (index !== -1) {
this.storage.activeDropbarExtensions.splice(index, 1);
}
},
}; };
}, },
}); });

View file

@ -81,6 +81,11 @@ export const getEditorRefHelpers = (args: TArgs): EditorRefApi => {
const markdownOutput = editor?.storage?.markdown?.getMarkdown?.(); const markdownOutput = editor?.storage?.markdown?.getMarkdown?.();
return markdownOutput; return markdownOutput;
}, },
isAnyDropbarOpen: () => {
if (!editor) return false;
const utilityStorage = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY);
return utilityStorage.activeDropbarExtensions.length > 0;
},
scrollSummary: (marking) => { scrollSummary: (marking) => {
if (!editor) return; if (!editor) return;
scrollSummary(editor, marking); scrollSummary(editor, marking);

View file

@ -119,6 +119,7 @@ export type EditorRefApi = {
getMarkDown: () => string; getMarkDown: () => string;
getSelectedText: () => string | null; getSelectedText: () => string | null;
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void; insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
isAnyDropbarOpen: () => boolean;
isEditorReadyToDiscard: () => boolean; isEditorReadyToDiscard: () => boolean;
isMenuItemActive: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => boolean; isMenuItemActive: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => boolean;
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined; listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;