From ab2a5ffb9a8f6d29434bfe341715abf3c8731812 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 20 Apr 2026 21:07:49 +0300 Subject: [PATCH] =?UTF-8?q?UI=20-=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E?= =?UTF-8?q?=D0=95=D0=9A=D0=A2=D0=9D=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C?= =?UTF-8?q?=D0=A3=D0=9D=D0=98=D0=9A=D0=90=D0=A6=D0=98=D0=AF:=20=D1=83?= =?UTF-8?q?=D0=BD=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20shell?= =?UTF-8?q?=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=BD=D0=B5=D1=88=D0=BD=D0=B5=D0=B3=D0=BE=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D1=83=D1=80=D0=B0=20=D1=81=20=D0=B2=D0=BD=D1=83=D1=82?= =?UTF-8?q?=D1=80=D0=B5=D0=BD=D0=BD=D0=B8=D0=BC=20peek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../external-contours/content-root.tsx | 57 +++-- .../external-contours/issue-header.tsx | 241 +++++++++++++++--- .../projects/external-contours/peek-shell.tsx | 208 +++++++++++++++ .../projects/external-contours/root.tsx | 23 +- .../external-contours/subscription.tsx | 110 ++++++++ .../workspace-notifications/root.tsx | 2 + 6 files changed, 563 insertions(+), 78 deletions(-) create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/peek-shell.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/subscription.tsx diff --git a/plane-src/apps/web/ce/components/projects/external-contours/content-root.tsx b/plane-src/apps/web/ce/components/projects/external-contours/content-root.tsx index fbe9c7b..40180c7 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/content-root.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/content-root.tsx @@ -9,21 +9,28 @@ import { observer } from "mobx-react"; import useSWR from "swr"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import type { TNameDescriptionLoader } from "@plane/types"; -import { ContentWrapper } from "@plane/ui"; import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours"; import { useUser, useUserPermissions } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; -import { ExternalContoursIssueActionsHeader } from "./issue-header"; +import { ExternalContoursPeekShell } from "./peek-shell"; import { ExternalContoursIssueMainContent } from "./issue-root"; type Props = { workspaceSlug: string; projectId: string; inboxIssueId: string; + embedIssue?: boolean; + embedRemoveCurrentNotification?: () => void; }; export const ExternalContoursContentRoot = observer(function ExternalContoursContentRoot(props: Props) { - const { workspaceSlug, projectId, inboxIssueId } = props; + const { + workspaceSlug, + projectId, + inboxIssueId, + embedIssue = false, + embedRemoveCurrentNotification, + } = props; const router = useAppRouter(); const [isSubmitting, setIsSubmitting] = useState("saved"); const [isDetailResolved, setIsDetailResolved] = useState(false); @@ -81,28 +88,26 @@ export const ExternalContoursContentRoot = observer(function ExternalContoursCon if (!contourRequest || !issue) return <>; return ( -
-
- -
- - - -
+ router.replace(`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}`)} + > + + ); }); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/issue-header.tsx b/plane-src/apps/web/ce/components/projects/external-contours/issue-header.tsx index 3b4a677..eb19e60 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/issue-header.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/issue-header.tsx @@ -4,22 +4,57 @@ * See the LICENSE file for details. */ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +import Link from "next/link"; +import { MoreHorizontal, MoveDiagonal, MoveRight } from "lucide-react"; import { useTranslation } from "@plane/i18n"; +import { IconButton, getIconButtonStyling } from "@plane/propel/icon-button"; import { Button } from "@plane/propel/button"; -import { CheckCircleFilledIcon, ChevronDownIcon, ChevronUpIcon, CloseCircleFilledIcon, LinkIcon, NewTabIcon } from "@plane/propel/icons"; +import { + CenterPanelIcon, + CheckCircleFilledIcon, + ChevronDownIcon, + ChevronUpIcon, + CloseCircleFilledIcon, + CopyLinkIcon, + FullScreenPanelIcon, + NewTabIcon, + SidePanelIcon, +} from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TExternalContourRequest, TNameDescriptionLoader } from "@plane/types"; import { EInboxIssueCurrentTab } from "@plane/types"; -import { ControlLink, Header, Row } from "@plane/ui"; +import { ControlLink, CustomMenu, CustomSelect, Header, Row, Tooltip } from "@plane/ui"; import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils"; import { NameDescriptionUpdateStatus } from "@/components/issues/issue-update-status"; import { useProject } from "@/hooks/store/use-project"; +import { useProjectExternalContoursBoard } from "@/hooks/store/use-project-external-contours-board"; import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours"; import { useAppRouter } from "@/hooks/use-app-router"; import { ExternalContourStatePill } from "./state-pill"; import { ExternalContourDeclineModal } from "./decline-modal"; +import { ExternalContourSubscription } from "./subscription"; + +export type TExternalContourPeekMode = "side-peek" | "modal" | "full-screen"; + +const PEEK_OPTIONS: { key: TExternalContourPeekMode; icon: any; i18n_title: string }[] = [ + { + key: "side-peek", + icon: SidePanelIcon, + i18n_title: "common.side_peek", + }, + { + key: "modal", + icon: CenterPanelIcon, + i18n_title: "common.modal", + }, + { + key: "full-screen", + icon: FullScreenPanelIcon, + i18n_title: "common.full_screen", + }, +]; type Props = { workspaceSlug: string; @@ -27,6 +62,11 @@ type Props = { contourRequest: TExternalContourRequest; hasDirectTargetAccess: boolean; isSubmitting: TNameDescriptionLoader; + removeRoutePeekId: () => void; + peekMode: TExternalContourPeekMode; + setPeekMode: (value: TExternalContourPeekMode) => void; + currentTab: TInboxIssueCurrentTab; + embedIssue?: boolean; }; export const ExternalContoursIssueActionsHeader = observer(function ExternalContoursIssueActionsHeader(props: Props) { @@ -36,16 +76,27 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont contourRequest, hasDirectTargetAccess, isSubmitting, + removeRoutePeekId, + peekMode, + setPeekMode, + currentTab, + embedIssue = false, } = props; + const parentRef = useRef(null); const { t } = useTranslation(); const router = useAppRouter(); const [isDeclineModalOpen, setIsDeclineModalOpen] = useState(false); - const { currentTab, decideRequest, filteredRequestIds, handleCurrentTab, loader } = useProjectExternalContours(); + const { decideRequest, filteredRequestIds, handleCurrentTab, loader } = useProjectExternalContours(); + const { currentTab: boardCurrentTab, columnIdsMap } = useProjectExternalContoursBoard(); const { getProjectById } = useProject(); const issue = contourRequest.issue; const currentRequestId = contourRequest.id; - const hasRelativeNavigation = !!currentRequestId && filteredRequestIds.includes(currentRequestId); + const boardRequestIds = + boardCurrentTab === currentTab ? [...(columnIdsMap.outgoing ?? []), ...(columnIdsMap.incoming ?? [])] : []; + const relativeRequestIds = boardRequestIds.includes(currentRequestId) ? boardRequestIds : filteredRequestIds; + const currentMode = PEEK_OPTIONS.find((mode) => mode.key === peekMode); + const hasRelativeNavigation = !!currentRequestId && relativeRequestIds.includes(currentRequestId); const canReviewClosedRequest = contourRequest.capabilities?.can_source_decide ?? (contourRequest.status === "closed" && contourRequest.source_decision !== "accepted"); @@ -53,18 +104,18 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont const redirectToRelativeIssue = useCallback( (direction: "next" | "prev") => { - if (!filteredRequestIds || !currentRequestId || !hasRelativeNavigation || filteredRequestIds.length <= 1) return; - const currentIssueIndex = filteredRequestIds.findIndex((requestId) => requestId === currentRequestId); + if (!relativeRequestIds || !currentRequestId || !hasRelativeNavigation || relativeRequestIds.length <= 1) return; + const currentIssueIndex = relativeRequestIds.findIndex((requestId) => requestId === currentRequestId); if (currentIssueIndex === -1) return; const nextIssueIndex = direction === "next" - ? (currentIssueIndex + 1) % filteredRequestIds.length - : (currentIssueIndex - 1 + filteredRequestIds.length) % filteredRequestIds.length; - const nextIssueId = filteredRequestIds[nextIssueIndex]; + ? (currentIssueIndex + 1) % relativeRequestIds.length + : (currentIssueIndex - 1 + relativeRequestIds.length) % relativeRequestIds.length; + const nextIssueId = relativeRequestIds[nextIssueIndex]; if (!nextIssueId) return; router.push(`/${workspaceSlug}/projects/${sourceProjectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${nextIssueId}`); }, - [currentRequestId, currentTab, filteredRequestIds, hasRelativeNavigation, router, sourceProjectId, workspaceSlug] + [currentRequestId, currentTab, hasRelativeNavigation, relativeRequestIds, router, sourceProjectId, workspaceSlug] ); useEffect(() => { @@ -78,6 +129,8 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont }, [redirectToRelativeIssue]); const targetProjectIdentifier = issue.project_detail?.identifier || getProjectById(issue.project_id || "")?.identifier; + const requestTab = contourRequest.status === "closed" ? EInboxIssueCurrentTab.CLOSED : EInboxIssueCurrentTab.OPEN; + const requestLink = `/${workspaceSlug}/projects/${sourceProjectId}/external-contours?currentTab=${requestTab}&inboxIssueId=${contourRequest.id}`; const workItemLink = generateWorkItemLink({ workspaceSlug: workspaceSlug?.toString(), projectId: issue.project_id, @@ -87,7 +140,7 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont }); const handleCopyLink = () => - copyUrlToClipboard(workItemLink).then(() => + copyUrlToClipboard(requestLink).then(() => setToast({ type: TOAST_TYPE.SUCCESS, title: t("common.link_copied"), @@ -132,26 +185,72 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont onSubmit={(comment) => handleDecision("decline", comment)} /> - -
+