/** * Copyright (c) 2023-present Plane Software, Inc. and contributors * SPDX-License-Identifier: AGPL-3.0-only * See the LICENSE file for details. */ import { createPortal } from "react-dom"; import { useCallback, useEffect, useRef, useState, type MouseEvent as ReactMouseEvent, type ReactNode } from "react"; import { observer } from "mobx-react"; import { cn } from "@plane/utils"; import type { TExternalContourRequest, TNameDescriptionLoader } from "@plane/types"; import useKeypress from "@/hooks/use-keypress"; import usePeekOverviewOutsideClickDetector from "@/hooks/use-peek-overview-outside-click"; import { ExternalContoursIssueActionsHeader, type TExternalContourPeekMode } from "./issue-header"; const SIDE_PEEK_WIDTH_STORAGE_KEY = "nodedc:external-contour-peek-width"; const SIDE_PEEK_MIN_WIDTH = 780; type Props = { workspaceSlug: string; projectId: string; contourRequest: TExternalContourRequest; hasDirectTargetAccess: boolean; isSubmitting: TNameDescriptionLoader; embedIssue?: boolean; embedRemoveCurrentNotification?: () => void; children: ReactNode; onClose: () => void; }; export const ExternalContoursPeekShell = observer(function ExternalContoursPeekShell(props: Props) { const { workspaceSlug, projectId, contourRequest, hasDirectTargetAccess, isSubmitting, embedIssue = false, embedRemoveCurrentNotification, children, onClose, } = props; const [peekMode, setPeekMode] = useState("side-peek"); const [sidePeekWidth, setSidePeekWidth] = useState(() => { if (typeof window === "undefined") return SIDE_PEEK_MIN_WIDTH; const fallbackWidth = Math.max(SIDE_PEEK_MIN_WIDTH, Math.floor(window.innerWidth * 0.54)); const storedWidth = window.localStorage.getItem(SIDE_PEEK_WIDTH_STORAGE_KEY); const parsedWidth = storedWidth ? parseInt(storedWidth, 10) : NaN; return Number.isFinite(parsedWidth) ? parsedWidth : fallbackWidth; }); const [isResizingPeek, setIsResizingPeek] = useState(false); const issuePeekOverviewRef = useRef(null); const initialPeekWidthRef = useRef(0); const initialMouseXRef = useRef(0); const removeRoutePeekId = useCallback(() => { if (embedIssue) { embedRemoveCurrentNotification?.(); return; } onClose(); }, [embedIssue, embedRemoveCurrentNotification, onClose]); const stopPeekResizing = useCallback(() => { setIsResizingPeek(false); }, []); const handlePeekResize = useCallback( (event: MouseEvent) => { if (!isResizingPeek) return; const maxWidth = Math.max(SIDE_PEEK_MIN_WIDTH, window.innerWidth - 48); const deltaX = event.clientX - initialMouseXRef.current; const nextWidth = Math.min(Math.max(initialPeekWidthRef.current - deltaX, SIDE_PEEK_MIN_WIDTH), maxWidth); setSidePeekWidth(nextWidth); }, [isResizingPeek] ); const startPeekResizing = useCallback( (event: ReactMouseEvent) => { if (peekMode !== "side-peek") return; event.preventDefault(); setIsResizingPeek(true); initialPeekWidthRef.current = sidePeekWidth; initialMouseXRef.current = event.clientX; }, [peekMode, sidePeekWidth] ); useEffect(() => { const handleWindowResize = () => { const maxWidth = Math.max(SIDE_PEEK_MIN_WIDTH, window.innerWidth - 48); setSidePeekWidth((currentWidth) => Math.min(currentWidth, maxWidth)); }; window.addEventListener("resize", handleWindowResize); return () => window.removeEventListener("resize", handleWindowResize); }, []); useEffect(() => { if (typeof window === "undefined") return; const maxWidth = Math.max(SIDE_PEEK_MIN_WIDTH, window.innerWidth - 48); const clampedWidth = Math.min(Math.max(sidePeekWidth, SIDE_PEEK_MIN_WIDTH), maxWidth); window.localStorage.setItem(SIDE_PEEK_WIDTH_STORAGE_KEY, String(clampedWidth)); if (clampedWidth !== sidePeekWidth) { setSidePeekWidth(clampedWidth); } }, [sidePeekWidth]); useEffect(() => { if (!isResizingPeek) return; document.addEventListener("mousemove", handlePeekResize); document.addEventListener("mouseup", stopPeekResizing); document.addEventListener("mouseleave", stopPeekResizing); window.addEventListener("blur", stopPeekResizing); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; return () => { document.removeEventListener("mousemove", handlePeekResize); document.removeEventListener("mouseup", stopPeekResizing); document.removeEventListener("mouseleave", stopPeekResizing); window.removeEventListener("blur", stopPeekResizing); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; }, [handlePeekResize, isResizingPeek, stopPeekResizing]); usePeekOverviewOutsideClickDetector( issuePeekOverviewRef, () => { if (!embedIssue) removeRoutePeekId(); }, contourRequest.id, ["main-sidebar"] ); useKeypress("Escape", () => !embedIssue && removeRoutePeekId()); const peekOverviewClassName = cn( !embedIssue ? "absolute z-[80] flex flex-col overflow-hidden border border-subtle/70 bg-surface-1/80 backdrop-blur-2xl transition-all duration-300" : "h-full w-full", !embedIssue && { "top-[5.35rem] right-3 bottom-[5.85rem] w-[calc(100%-1.5rem)] rounded-[28px] border md:min-w-[780px] md:max-w-[calc(100vw-1.5rem)]": peekMode === "side-peek", "top-[8.33%] left-[8.33%] size-5/6 rounded-[28px]": peekMode === "modal", "absolute inset-0 m-4 rounded-[28px]": peekMode === "full-screen", } ); const portalContainer = typeof document !== "undefined" ? document.getElementById("full-screen-portal") : null; const content = (
{!embedIssue && peekMode === "side-peek" && (
)}
{["side-peek", "modal"].includes(peekMode) ? (
{children}
) : (
{children}
)}
); return <>{!embedIssue && portalContainer ? createPortal(content, portalContainer) : content}; });