From 36d328445c602ca995681f7ba9299cb3f8c86aaa Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Mon, 22 Sep 2025 18:04:16 +0530 Subject: [PATCH 1/4] [WIKI-650] fix: pane extensions close method moved into hook (#7823) * fix: pane extensions close method moved into hook * fix: editor build breaks web everytime in dev mode * fix: variant of lite text toolbar --- .../hooks/pages/use-pages-pane-extensions.ts | 9 ++ .../components/editor/lite-text/editor.tsx | 105 ++++++++++++------ .../editor/lite-text/lite-toolbar.tsx | 33 ++++++ .../components/pages/editor/page-root.tsx | 30 ++--- apps/web/core/constants/editor.ts | 11 +- packages/editor/tsdown.config.ts | 2 +- 6 files changed, 132 insertions(+), 58 deletions(-) create mode 100644 apps/web/core/components/editor/lite-text/lite-toolbar.tsx diff --git a/apps/web/ce/hooks/pages/use-pages-pane-extensions.ts b/apps/web/ce/hooks/pages/use-pages-pane-extensions.ts index cd405c1ca4..b73ffc58b0 100644 --- a/apps/web/ce/hooks/pages/use-pages-pane-extensions.ts +++ b/apps/web/ce/hooks/pages/use-pages-pane-extensions.ts @@ -4,6 +4,7 @@ import type { EditorRefApi } from "@plane/editor"; import { PAGE_NAVIGATION_PANE_TAB_KEYS, PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, + PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, } from "@/components/pages/navigation-pane"; import { useAppRouter } from "@/hooks/use-app-router"; import { useQueryParams } from "@/hooks/use-query-params"; @@ -43,10 +44,18 @@ export const usePagesPaneExtensions = (_params: TPageExtensionHookParams) => { const navigationPaneExtensions: INavigationPaneExtension[] = []; + const handleCloseNavigationPane = useCallback(() => { + const updatedRoute = updateQueryParams({ + paramsToRemove: [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM], + }); + router.push(updatedRoute); + }, [router, updateQueryParams]); + return { editorExtensionHandlers, navigationPaneExtensions, handleOpenNavigationPane, isNavigationPaneOpen, + handleCloseNavigationPane, }; }; diff --git a/apps/web/core/components/editor/lite-text/editor.tsx b/apps/web/core/components/editor/lite-text/editor.tsx index 42d6f0bd95..7e1cf4f761 100644 --- a/apps/web/core/components/editor/lite-text/editor.tsx +++ b/apps/web/core/components/editor/lite-text/editor.tsx @@ -14,8 +14,9 @@ import { useEditorConfig, useEditorMention } from "@/hooks/editor"; import { useMember } from "@/hooks/store/use-member"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; -// plane web services +// plane web service import { WorkspaceService } from "@/plane-web/services"; +import { LiteToolbar } from "./lite-toolbar"; const workspaceService = new WorkspaceService(); type LiteTextEditorWrapperProps = MakeOptional< @@ -31,9 +32,10 @@ type LiteTextEditorWrapperProps = MakeOptional< showSubmitButton?: boolean; isSubmitting?: boolean; showToolbarInitially?: boolean; - showToolbar?: boolean; + variant?: "full" | "lite" | "none"; issue_id?: string; parentClassName?: string; + editorClassName?: string; } & ( | { editable: false; @@ -59,14 +61,17 @@ export const LiteTextEditor = React.forwardRef !showToolbarInitially && setIsFocused(true)} - onBlur={() => !showToolbarInitially && setIsFocused(false)} + onFocus={() => isFullVariant && !showToolbarInitially && setIsFocused(true)} + onBlur={() => isFullVariant && !showToolbarInitially && setIsFocused(false)} > - "", - workspaceId, - workspaceSlug, - })} - mentionHandler={{ - searchCallback: async (query) => { - const res = await fetchMentions(query); - if (!res) throw new Error("Failed in fetching mentions"); - return res; - }, - renderComponent: EditorMentionsRoot, - getMentionedEntityDetails: (id) => ({ - display_name: getUserDetails(id)?.display_name ?? "", - }), - }} - placeholder={placeholder} - containerClassName={cn(containerClassName, "relative", { - "p-2": !editable, - })} - extendedEditorProps={{}} - {...rest} - /> - {showToolbar && editable && ( + {/* Wrapper for lite toolbar layout */} +
+ {/* Main Editor - always rendered once */} +
+ "", + workspaceId, + workspaceSlug, + })} + mentionHandler={{ + searchCallback: async (query) => { + const res = await fetchMentions(query); + if (!res) throw new Error("Failed in fetching mentions"); + return res; + }, + renderComponent: EditorMentionsRoot, + getMentionedEntityDetails: (id) => ({ + display_name: getUserDetails(id)?.display_name ?? "", + }), + }} + placeholder={placeholder} + containerClassName={cn(containerClassName, "relative", { + "p-2": !editable, + })} + extendedEditorProps={{}} + editorClassName={editorClassName} + {...rest} + /> +
+ + {/* Lite Toolbar - conditionally rendered */} + {isLiteVariant && editable && ( + { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + editorRef?.executeMenuItemCommand({ + itemKey: item.itemKey, + ...item.extraProps, + }); + }} + onSubmit={(e) => rest.onEnterKeyPress?.(e)} + isSubmitting={isSubmitting} + isEmpty={isEmpty} + /> + )} +
+ + {/* Full Toolbar - conditionally rendered */} + {isFullVariant && editable && (
| React.MouseEvent) => void; + isSubmitting: boolean; + isEmpty: boolean; + executeCommand: (item: ToolbarMenuItem) => void; +}; + +export const LiteToolbar = ({ onSubmit, isSubmitting, isEmpty, executeCommand }: LiteToolbarProps) => ( +
+ + +
+); + +export type { LiteToolbarProps }; diff --git a/apps/web/core/components/pages/editor/page-root.tsx b/apps/web/core/components/pages/editor/page-root.tsx index 3159691e13..487592b019 100644 --- a/apps/web/core/components/pages/editor/page-root.tsx +++ b/apps/web/core/components/pages/editor/page-root.tsx @@ -6,7 +6,6 @@ import type { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryPara // hooks import { useAppRouter } from "@/hooks/use-app-router"; import { usePageFallback } from "@/hooks/use-page-fallback"; -import { useQueryParams } from "@/hooks/use-query-params"; // plane web import import { PageModals } from "@/plane-web/components/pages"; import { usePagesPaneExtensions, useExtendedEditorProps } from "@/plane-web/hooks/pages"; @@ -14,11 +13,7 @@ import { EPageStoreType } from "@/plane-web/hooks/store"; // store import type { TPageInstance } from "@/store/pages/base-page"; // local imports -import { - PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, - PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, - PageNavigationPaneRoot, -} from "../navigation-pane"; +import { PageNavigationPaneRoot } from "../navigation-pane"; import { PageVersionsOverlay } from "../version"; import { PagesVersionEditor } from "../version/editor"; import { PageEditorBody, type TEditorBodyConfig, type TEditorBodyHandlers } from "./editor-body"; @@ -66,7 +61,6 @@ export const PageRoot = observer((props: TPageRootProps) => { hasConnectionFailed, updatePageDescription: handlers.updateDescription, }); - const { updateQueryParams } = useQueryParams(); const handleEditorReady = useCallback( (status: boolean) => { @@ -85,11 +79,16 @@ export const PageRoot = observer((props: TPageRootProps) => { }, [isContentEditable, setEditorRef]); // Get extensions and navigation logic from hook - const { editorExtensionHandlers, navigationPaneExtensions, handleOpenNavigationPane, isNavigationPaneOpen } = - usePagesPaneExtensions({ - page, - editorRef, - }); + const { + editorExtensionHandlers, + navigationPaneExtensions, + handleOpenNavigationPane, + handleCloseNavigationPane, + isNavigationPaneOpen, + } = usePagesPaneExtensions({ + page, + editorRef, + }); // Get extended editor extensions configuration const extendedEditorProps = useExtendedEditorProps({ @@ -118,13 +117,6 @@ export const PageRoot = observer((props: TPageRootProps) => { [setEditorRef] ); - const handleCloseNavigationPane = useCallback(() => { - const updatedRoute = updateQueryParams({ - paramsToRemove: [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM], - }); - router.push(updatedRoute); - }, [router, updateQueryParams]); - return (
diff --git a/apps/web/core/constants/editor.ts b/apps/web/core/constants/editor.ts index ff046b917d..c6bc2fef40 100644 --- a/apps/web/core/constants/editor.ts +++ b/apps/web/core/constants/editor.ts @@ -158,9 +158,18 @@ const USER_ACTION_ITEMS: ToolbarMenuItem<"quote" | "code">[] = [ { itemKey: "code", renderKey: "code", name: "Code", icon: Code2, editors: ["lite", "document"] }, ]; +export const IMAGE_ITEM = { + itemKey: "image", + renderKey: "image", + name: "Image", + icon: Image, + editors: ["lite", "document"], + extraProps: {}, +} as ToolbarMenuItem<"image">; + const COMPLEX_ITEMS: ToolbarMenuItem<"table" | "image">[] = [ { itemKey: "table", renderKey: "table", name: "Table", icon: Table, editors: ["document"] }, - { itemKey: "image", renderKey: "image", name: "Image", icon: Image, editors: ["lite", "document"], extraProps: {} }, + IMAGE_ITEM, ]; export const TOOLBAR_ITEMS: { diff --git a/packages/editor/tsdown.config.ts b/packages/editor/tsdown.config.ts index 348f1fd7a7..5e9a7b960a 100644 --- a/packages/editor/tsdown.config.ts +++ b/packages/editor/tsdown.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ outDir: "dist", format: ["esm", "cjs"], dts: true, - clean: true, + clean: false, sourcemap: true, copy: ["src/styles"], }); From 14e3aace92304a8aff5e25e8c55e83f359f402c1 Mon Sep 17 00:00:00 2001 From: Vipin Chaudhary Date: Mon, 22 Sep 2025 18:07:52 +0530 Subject: [PATCH 2/4] [WIKI-623] fix: add block menu to rich text editor (#7813) * fix : block menu for rich editor * chore: remove comments * chore : update selection logic --- .../components/editors/rich-text/editor.tsx | 9 +- .../src/core/components/menus/block-menu.tsx | 212 +++++++++++------- 2 files changed, 142 insertions(+), 79 deletions(-) diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index f110c789b2..40a2b0c0be 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -1,7 +1,7 @@ import { forwardRef, useCallback } from "react"; // components import { EditorWrapper } from "@/components/editors"; -import { EditorBubbleMenu } from "@/components/menus"; +import { BlockMenu, EditorBubbleMenu } from "@/components/menus"; // extensions import { SideMenuExtension } from "@/extensions"; // plane editor imports @@ -40,7 +40,12 @@ const RichTextEditor: React.FC = (props) => { return ( - {(editor) => <>{editor && bubbleMenuEnabled && }} + {(editor) => ( + <> + {editor && bubbleMenuEnabled && } + + + )} ); }; diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index 43c0193c71..e8a644c1fa 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -1,8 +1,18 @@ +import { + useFloating, + offset, + flip, + shift, + autoUpdate, + useDismiss, + useInteractions, + FloatingPortal, +} from "@floating-ui/react"; import type { Editor } from "@tiptap/react"; import { Copy, LucideIcon, Trash2 } from "lucide-react"; -import { useCallback, useEffect, useRef } from "react"; -import tippy, { Instance } from "tippy.js"; +import { useCallback, useEffect, useRef, useState } from "react"; // constants +import { cn } from "@plane/utils"; import { CORE_EXTENSIONS } from "@/constants/extension"; import { IEditorProps } from "@/types"; @@ -14,62 +24,73 @@ type Props = { export const BlockMenu = (props: Props) => { const { editor } = props; - const menuRef = useRef(null); - const popup = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [isAnimatedIn, setIsAnimatedIn] = useState(false); + const menuRef = useRef(null); + const virtualReferenceRef = useRef<{ getBoundingClientRect: () => DOMRect }>({ + getBoundingClientRect: () => new DOMRect(), + }); - const handleClickDragHandle = useCallback((event: MouseEvent) => { - const target = event.target as HTMLElement; - if (target.matches("#drag-handle")) { - event.preventDefault(); + // Set up Floating UI with virtual reference element + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + middleware: [offset({ crossAxis: -10 }), flip(), shift()], + whileElementsMounted: autoUpdate, + placement: "left-start", + }); - popup.current?.setProps({ - getReferenceClientRect: () => target.getBoundingClientRect(), - }); + const dismiss = useDismiss(context); + const { getFloatingProps } = useInteractions([dismiss]); - popup.current?.show(); - return; - } + // Handle click on drag handle + const handleClickDragHandle = useCallback( + (event: MouseEvent) => { + const target = event.target as HTMLElement; + const dragHandle = target.closest("#drag-handle"); - popup.current?.hide(); - return; - }, []); + if (dragHandle) { + event.preventDefault(); + + // Update virtual reference with current drag handle position + virtualReferenceRef.current = { + getBoundingClientRect: () => dragHandle.getBoundingClientRect(), + }; + + // Set the virtual reference as the reference element + refs.setReference(virtualReferenceRef.current); + + // Ensure the targeted block is selected + const rect = dragHandle.getBoundingClientRect(); + const coords = { left: rect.left + rect.width / 2, top: rect.top + rect.height / 2 }; + const posAtCoords = editor.view.posAtCoords(coords); + if (posAtCoords) { + const $pos = editor.state.doc.resolve(posAtCoords.pos); + const nodePos = $pos.before($pos.depth); + editor.chain().setNodeSelection(nodePos).run(); + } + // Show the menu + setIsOpen(true); + return; + } + + // If clicking outside and not on a menu item, hide the menu + if (menuRef.current && !menuRef.current.contains(target)) { + setIsOpen(false); + } + }, + [refs] + ); useEffect(() => { - if (menuRef.current) { - menuRef.current.remove(); - menuRef.current.style.visibility = "visible"; - - // @ts-expect-error - Tippy types are incorrect - popup.current = tippy(document.body, { - getReferenceClientRect: null, - content: menuRef.current, - appendTo: () => document.querySelector(".frame-renderer"), - trigger: "manual", - interactive: true, - arrow: false, - placement: "left-start", - animation: "shift-away", - maxWidth: 500, - hideOnClick: true, - onShown: () => { - menuRef.current?.focus(); - }, - }); - } - - return () => { - popup.current?.destroy(); - popup.current = null; - }; - }, []); - - useEffect(() => { - const handleKeyDown = () => { - popup.current?.hide(); + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsOpen(false); + } }; const handleScroll = () => { - popup.current?.hide(); + setIsOpen(false); }; document.addEventListener("click", handleClickDragHandle); document.addEventListener("contextmenu", handleClickDragHandle); @@ -84,6 +105,23 @@ export const BlockMenu = (props: Props) => { }; }, [handleClickDragHandle]); + // Animation effect + useEffect(() => { + if (isOpen) { + setIsAnimatedIn(false); + // Add a small delay before starting the animation + const timeout = setTimeout(() => { + requestAnimationFrame(() => { + setIsAnimatedIn(true); + }); + }, 50); + + return () => clearTimeout(timeout); + } else { + setIsAnimatedIn(false); + } + }, [isOpen]); + const MENU_ITEMS: { icon: LucideIcon; key: string; @@ -96,10 +134,13 @@ export const BlockMenu = (props: Props) => { key: "delete", label: "Delete", onClick: (e) => { - editor.chain().deleteSelection().focus().run(); - popup.current?.hide(); e.preventDefault(); e.stopPropagation(); + + // Execute the delete action + editor.chain().deleteSelection().focus().run(); + + setIsOpen(false); }, }, { @@ -146,36 +187,53 @@ export const BlockMenu = (props: Props) => { console.error(error.message); } } - - popup.current?.hide(); + setIsOpen(false); }, }, ]; + if (!isOpen) { + return null; + } return ( -
- {MENU_ITEMS.map((item) => { - // Skip rendering the button if it should be disabled - if (item.isDisabled && item.key === "duplicate") { - return null; - } - - return ( - - ); - })} -
+ +
{ + refs.setFloating(node); + menuRef.current = node; + }} + style={{ + ...floatingStyles, + zIndex: 99, + animationFillMode: "forwards", + transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)", // Expo ease out + }} + className={cn( + "z-20 max-h-60 min-w-[7rem] overflow-y-scroll rounded-lg border border-custom-border-200 bg-custom-background-100 p-1.5 shadow-custom-shadow-rg", + "transition-all duration-300 transform origin-top-right", + isAnimatedIn ? "opacity-100 scale-100" : "opacity-0 scale-75" + )} + data-prevent-outside-click + {...getFloatingProps()} + > + {MENU_ITEMS.map((item) => { + if (item.isDisabled) { + return null; + } + return ( + + ); + })} +
+
); }; From 3ec184e883dd13aebe6ffa58819790b1f3308200 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Mon, 22 Sep 2025 18:24:29 +0530 Subject: [PATCH 3/4] fix: block menu ref --- .../components/editor/lite-text/editor.tsx | 88 ++++--- .../src/core/components/menus/block-menu.tsx | 222 ++++++++++++------ 2 files changed, 204 insertions(+), 106 deletions(-) diff --git a/apps/web/core/components/editor/lite-text/editor.tsx b/apps/web/core/components/editor/lite-text/editor.tsx index 5bdec0b45c..43c264a77c 100644 --- a/apps/web/core/components/editor/lite-text/editor.tsx +++ b/apps/web/core/components/editor/lite-text/editor.tsx @@ -111,36 +111,64 @@ export const LiteTextEditor = React.forwardRef isFullVariant && !showToolbarInitially && setIsFocused(true)} onBlur={() => isFullVariant && !showToolbarInitially && setIsFocused(false)} > - "", - workspaceId, - workspaceSlug, - })} - mentionHandler={{ - searchCallback: async (query) => { - const res = await fetchMentions(query); - if (!res) throw new Error("Failed in fetching mentions"); - return res; - }, - renderComponent: EditorMentionsRoot, - getMentionedEntityDetails: (id) => ({ - display_name: getUserDetails(id)?.display_name ?? "", - }), - }} - placeholder={placeholder} - containerClassName={cn(containerClassName, "relative", { - "p-2": !editable, - })} - extendedEditorProps={{}} - {...rest} - /> - {showToolbar && editable && ( + {/* Wrapper for lite toolbar layout */} +
+ {/* Main Editor - always rendered once */} +
+ "", + workspaceId, + workspaceSlug, + })} + mentionHandler={{ + searchCallback: async (query) => { + const res = await fetchMentions(query); + if (!res) throw new Error("Failed in fetching mentions"); + return res; + }, + renderComponent: EditorMentionsRoot, + getMentionedEntityDetails: (id) => ({ + display_name: getUserDetails(id)?.display_name ?? "", + }), + }} + placeholder={placeholder} + containerClassName={cn(containerClassName, "relative", { + "p-2": !editable, + })} + extendedEditorProps={{ + isSmoothCursorEnabled: is_smooth_cursor_enabled, + }} + editorClassName={editorClassName} + {...rest} + /> +
+ + {/* Lite Toolbar - conditionally rendered */} + {isLiteVariant && editable && ( + { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + editorRef?.executeMenuItemCommand({ + itemKey: item.itemKey, + ...item.extraProps, + }); + }} + onSubmit={(e) => rest.onEnterKeyPress?.(e)} + isSubmitting={isSubmitting} + isEmpty={isEmpty} + /> + )} +
+ + {/* Full Toolbar - conditionally rendered */} + {isFullVariant && editable && (
{ const { editor } = props; - const menuRef = useRef(null); - const popup = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [isAnimatedIn, setIsAnimatedIn] = useState(false); + const menuRef = useRef(null); + const virtualReferenceRef = useRef<{ getBoundingClientRect: () => DOMRect }>({ + getBoundingClientRect: () => new DOMRect(), + }); + // const { t } = useTranslation(); + const isEmbedFlagged = + props.flaggedExtensions?.includes("external-embed") || props.disabledExtensions?.includes("external-embed"); - const handleClickDragHandle = useCallback((event: MouseEvent) => { - const target = event.target as HTMLElement; - if (target.matches("#drag-handle")) { - event.preventDefault(); + // Set up Floating UI with virtual reference element + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + middleware: [offset({ crossAxis: -10 }), flip(), shift()], + whileElementsMounted: autoUpdate, + placement: "left-start", + }); - popup.current?.setProps({ - getReferenceClientRect: () => target.getBoundingClientRect(), - }); + const dismiss = useDismiss(context); + const { getFloatingProps } = useInteractions([dismiss]); - popup.current?.show(); - return; - } + // Handle click on drag handle + const handleClickDragHandle = useCallback( + (event: MouseEvent) => { + const target = event.target as HTMLElement; + const dragHandle = target.closest("#drag-handle"); - popup.current?.hide(); - return; - }, []); + if (dragHandle) { + event.preventDefault(); + // Update virtual reference with current drag handle position + virtualReferenceRef.current = { + getBoundingClientRect: () => dragHandle.getBoundingClientRect(), + }; + + // Set the virtual reference as the reference element + refs.setReference(virtualReferenceRef.current); + + // Ensure the targeted block is selected + const rect = dragHandle.getBoundingClientRect(); + const coords = { left: rect.left + rect.width / 2, top: rect.top + rect.height / 2 }; + const posAtCoords = editor.view.posAtCoords(coords); + if (posAtCoords) { + const $pos = editor.state.doc.resolve(posAtCoords.pos); + const nodePos = $pos.before($pos.depth); + editor.chain().setNodeSelection(nodePos).run(); + } + // Show the menu + setIsOpen(true); + return; + } + + // If clicking outside and not on a menu item, hide the menu + if (menuRef.current && !menuRef.current.contains(target)) { + setIsOpen(false); + } + }, + [editor, refs] + ); + + const editorState = useEditorState({ + editor, + selector: ({ editor }) => { + const selection = editor.state.selection; + const content = selection.content().content; + const firstChild = content.firstChild; + let linkUrl: string | null = null; + const foundLinkMarks: string[] = []; + + const isEmbedActive = editor.isActive(ADDITIONAL_EXTENSIONS.EXTERNAL_EMBED); + const isRichCard = firstChild?.attrs[EExternalEmbedAttributeNames.IS_RICH_CARD]; + const isNotEmbeddable = firstChild?.attrs[EExternalEmbedAttributeNames.HAS_EMBED_FAILED]; + + if (firstChild) { + for (let i = 0; i < firstChild.childCount; i++) { + const node = firstChild.child(i); + const linkMarks = node.marks?.filter( + (mark) => mark.type.name === CORE_EXTENSIONS.CUSTOM_LINK && mark.attrs?.href + ); + + if (linkMarks && linkMarks.length > 0) { + linkMarks.forEach((mark) => { + foundLinkMarks.push(mark.attrs.href); + }); + } + } + if (firstChild.attrs.src) { + foundLinkMarks.push(firstChild.attrs.src); + } + } + + if (foundLinkMarks.length === 1) { + linkUrl = foundLinkMarks[0]; + } + + return { + isEmbedActive, + isLinkEmbeddable: isEmbedActive || !!linkUrl, + linkUrl, + isRichCard, + isNotEmbeddable, + }; + }, + }); + + // Set up event listeners useEffect(() => { - if (menuRef.current) { - menuRef.current.remove(); - menuRef.current.style.visibility = "visible"; - - // @ts-expect-error - Tippy types are incorrect - popup.current = tippy(document.body, { - getReferenceClientRect: null, - content: menuRef.current, - appendTo: () => document.querySelector(".frame-renderer"), - trigger: "manual", - interactive: true, - arrow: false, - placement: "left-start", - animation: "shift-away", - maxWidth: 500, - hideOnClick: true, - onShown: () => { - menuRef.current?.focus(); - }, - }); - } - - return () => { - popup.current?.destroy(); - popup.current = null; - }; - }, []); - - useEffect(() => { - const handleKeyDown = () => { - popup.current?.hide(); + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsOpen(false); + } }; const handleScroll = () => { @@ -243,7 +302,7 @@ export const BlockMenu = (props: Props) => { label: "Delete", onClick: (e) => { editor.chain().deleteSelection().focus().run(); - popup.current?.hide(); + setIsOpen(false); e.preventDefault(); e.stopPropagation(); @@ -315,37 +374,48 @@ export const BlockMenu = (props: Props) => { }, ]; - if (!isOpen) { - return null; - } - if (!isOpen) { return null; } return ( -
- {MENU_ITEMS.map((item) => { - // Skip rendering the button if it should be disabled - if (item.isDisabled && item.key === "duplicate") { - return null; - } - - return ( - - ); - })} -
+ +
{ + refs.setFloating(node); + menuRef.current = node; + }} + style={{ + ...floatingStyles, + zIndex: 99, + animationFillMode: "forwards", + transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)", // Expo ease out + }} + className={cn( + "z-20 max-h-60 min-w-[7rem] overflow-y-scroll rounded-lg border border-custom-border-200 bg-custom-background-100 p-1.5 shadow-custom-shadow-rg", + "transition-all duration-300 transform origin-top-right", + isAnimatedIn ? "opacity-100 scale-100" : "opacity-0 scale-75" + )} + {...getFloatingProps()} + > + {MENU_ITEMS.map((item) => { + if (item.isDisabled) { + return null; + } + return ( + + ); + })} +
+
); }; From 2b89a935fca6e461e8c56c3ef7e2a2830a6916ad Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Mon, 22 Sep 2025 18:38:09 +0530 Subject: [PATCH 4/4] fix:format --- .../automations/[automationId]/header.tsx | 8 ++-- .../details/sidebar/trigger/root.tsx | 15 ++++--- .../modals/create-update-modal.tsx | 8 ++-- .../automations/modals/delete-modal.tsx | 4 +- apps/web/styles/globals.css | 40 ++++++++----------- 5 files changed, 36 insertions(+), 39 deletions(-) diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/automations/[automationId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/automations/[automationId]/header.tsx index e21d7d3aeb..c14b879ba6 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/automations/[automationId]/header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/automations/[automationId]/header.tsx @@ -72,7 +72,7 @@ export const ProjectAutomationDetailsHeader = observer((props: TProps) => { .then(() => { captureSuccess({ eventName: AUTOMATION_TRACKER_EVENTS.ENABLE, - payload: { id: automationId } + payload: { id: automationId }, }); setToast({ title: t("automations.toasts.enable.success.title"), @@ -85,7 +85,7 @@ export const ProjectAutomationDetailsHeader = observer((props: TProps) => { captureError({ eventName: AUTOMATION_TRACKER_EVENTS.ENABLE, error: err?.message || "Enable failed", - payload: { id: automationId } + payload: { id: automationId }, }); setToast({ title: t("automations.toasts.enable.error.title"), @@ -101,7 +101,7 @@ export const ProjectAutomationDetailsHeader = observer((props: TProps) => { .then(() => { captureSuccess({ eventName: AUTOMATION_TRACKER_EVENTS.DISABLE, - payload: { id: automationId } + payload: { id: automationId }, }); setToast({ title: t("automations.toasts.disable.success.title"), @@ -114,7 +114,7 @@ export const ProjectAutomationDetailsHeader = observer((props: TProps) => { captureError({ eventName: AUTOMATION_TRACKER_EVENTS.DISABLE, error: err?.message || "Disable failed", - payload: { id: automationId } + payload: { id: automationId }, }); setToast({ title: t("automations.toasts.disable.error.title"), diff --git a/apps/web/ee/components/automations/details/sidebar/trigger/root.tsx b/apps/web/ee/components/automations/details/sidebar/trigger/root.tsx index 5cf8a39a20..28b2240f3a 100644 --- a/apps/web/ee/components/automations/details/sidebar/trigger/root.tsx +++ b/apps/web/ee/components/automations/details/sidebar/trigger/root.tsx @@ -3,7 +3,12 @@ import isEqual from "lodash/isEqual"; import { observer } from "mobx-react"; import { ChevronDown, Zap } from "lucide-react"; // plane imports -import { AUTOMATION_TRIGGER_SELECT_OPTIONS, DEFAULT_AUTOMATION_CONDITION_FILTER_EXPRESSION, AUTOMATION_TRACKER_ELEMENTS, AUTOMATION_TRACKER_EVENTS } from "@plane/constants"; +import { + AUTOMATION_TRIGGER_SELECT_OPTIONS, + DEFAULT_AUTOMATION_CONDITION_FILTER_EXPRESSION, + AUTOMATION_TRACKER_ELEMENTS, + AUTOMATION_TRACKER_EVENTS, +} from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // helpers import { @@ -82,13 +87,13 @@ export const AutomationDetailsSidebarTriggerRoot: React.FC = observer((pr }); captureSuccess({ eventName: AUTOMATION_TRACKER_EVENTS.TRIGGER_CREATED, - payload: { id: automationId, handler_name: selectedTriggerNodeHandlerName } + payload: { id: automationId, handler_name: selectedTriggerNodeHandlerName }, }); } catch (error) { console.error("Failed to create trigger:", error); captureError({ eventName: AUTOMATION_TRACKER_EVENTS.TRIGGER_CREATED, - payload: { id: automationId, handler_name: selectedTriggerNodeHandlerName } + payload: { id: automationId, handler_name: selectedTriggerNodeHandlerName }, }); } return; @@ -102,13 +107,13 @@ export const AutomationDetailsSidebarTriggerRoot: React.FC = observer((pr }); captureSuccess({ eventName: AUTOMATION_TRACKER_EVENTS.TRIGGER_UPDATED, - payload: { id: automationId, handler_name: selectedTriggerNodeHandlerName } + payload: { id: automationId, handler_name: selectedTriggerNodeHandlerName }, }); } catch (error) { console.error("Failed to update trigger handler:", error); captureError({ eventName: AUTOMATION_TRACKER_EVENTS.TRIGGER_UPDATED, - payload: { id: automationId, handler_name: selectedTriggerNodeHandlerName } + payload: { id: automationId, handler_name: selectedTriggerNodeHandlerName }, }); } } diff --git a/apps/web/ee/components/automations/modals/create-update-modal.tsx b/apps/web/ee/components/automations/modals/create-update-modal.tsx index a7901bc733..4d2320f3dc 100644 --- a/apps/web/ee/components/automations/modals/create-update-modal.tsx +++ b/apps/web/ee/components/automations/modals/create-update-modal.tsx @@ -63,7 +63,7 @@ export const CreateUpdateAutomationModal: React.FC = observer((props) => const res = await createAutomation(workspaceSlug, projectId, payload); captureSuccess({ eventName: AUTOMATION_TRACKER_EVENTS.CREATE, - payload: { id: res?.id } + payload: { id: res?.id }, }); if (res?.redirectionLink) { router.push(res?.redirectionLink); @@ -72,7 +72,7 @@ export const CreateUpdateAutomationModal: React.FC = observer((props) => captureError({ eventName: AUTOMATION_TRACKER_EVENTS.CREATE, error: error?.error || error?.message, - payload: { workspace_slug: workspaceSlug, project_id: projectId } + payload: { workspace_slug: workspaceSlug, project_id: projectId }, }); setToast({ type: TOAST_TYPE.ERROR, @@ -89,13 +89,13 @@ export const CreateUpdateAutomationModal: React.FC = observer((props) => await automation?.update(payload); captureSuccess({ eventName: AUTOMATION_TRACKER_EVENTS.UPDATE, - payload: { id: data.id } + payload: { id: data.id }, }); } catch (error: any) { captureError({ eventName: AUTOMATION_TRACKER_EVENTS.UPDATE, error: error?.error || error?.message, - payload: { id: data.id } + payload: { id: data.id }, }); setToast({ type: TOAST_TYPE.ERROR, diff --git a/apps/web/ee/components/automations/modals/delete-modal.tsx b/apps/web/ee/components/automations/modals/delete-modal.tsx index 53d61c58c1..e8e64346a9 100644 --- a/apps/web/ee/components/automations/modals/delete-modal.tsx +++ b/apps/web/ee/components/automations/modals/delete-modal.tsx @@ -41,7 +41,7 @@ export const DeleteAutomationModal: React.FC = observer((props) => { await handleDelete(); captureSuccess({ eventName: AUTOMATION_TRACKER_EVENTS.DELETE, - payload: { id: automationId } + payload: { id: automationId }, }); setToast({ type: TOAST_TYPE.SUCCESS, @@ -55,7 +55,7 @@ export const DeleteAutomationModal: React.FC = observer((props) => { captureError({ eventName: AUTOMATION_TRACKER_EVENTS.DELETE, error: error?.message || "Delete failed", - payload: { id: automationId } + payload: { id: automationId }, }); setToast({ type: TOAST_TYPE.ERROR, diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index 049160ddaa..e25d37fb81 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -1293,7 +1293,8 @@ input[type="color"].custom-color-picker { } @keyframes grow-shrink-vertically { - 0%, 100% { + 0%, + 100% { transform: scaleY(0.5); } 50% { @@ -1301,7 +1302,6 @@ input[type="color"].custom-color-picker { } } - .animate-vertical-scale { transform-origin: center; /* ensures it grows from both ends */ animation: grow-shrink-vertically 0.8s ease-in-out infinite; @@ -1373,41 +1373,33 @@ input[type="color"].custom-color-picker { } } - - - .shimmer { display: inline-block; background-color: rgb(var(--color-text-100)); - background-image: linear-gradient( - to left, - transparent 0%, - rgb(var(--color-text-400)) 50%, - transparent 100% - ); + background-image: linear-gradient(to left, transparent 0%, rgb(var(--color-text-400)) 50%, transparent 100%); background-position: -4rem top; background-repeat: no-repeat; - background-clip: text; /* non-prefixed fallback */ - -webkit-background-clip: text; /* required for Safari/WebKit */ + background-clip: text; /* non-prefixed fallback */ + -webkit-background-clip: text; /* required for Safari/WebKit */ -webkit-text-fill-color: transparent; -webkit-animation: shimmer-ltr 2.2s linear infinite; animation: shimmer-ltr 2.2s linear infinite; -webkit-background-size: 60% 100%; - background-size: 60% 100%; /* unprefixed too */ + background-size: 60% 100%; /* unprefixed too */ } @keyframes shimmer-ltr { - 0% { - background-position: -4rem top; /*50px*/ - } + 0% { + background-position: -4rem top; /*50px*/ + } - 70% { - background-position: 200% top; /*200px*/ - } + 70% { + background-position: 200% top; /*200px*/ + } - 100% { - background-position: 200% top; /*200px*/ - } -} \ No newline at end of file + 100% { + background-position: 200% top; /*200px*/ + } +}