/** * Copyright (c) 2023-present Plane Software, Inc. and contributors * SPDX-License-Identifier: AGPL-3.0-only * See the LICENSE file for details. */ import { useCallback, useEffect, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { MoveDiagonal, MoveRight } from "lucide-react"; import { useTranslation } from "@plane/i18n"; import { IconButton } from "@plane/propel/icon-button"; import { Button } from "@plane/propel/button"; import { CenterPanelIcon, CheckCircleFilledIcon, ChevronDownIcon, ChevronUpIcon, CloseCircleFilledIcon, CopyLinkIcon, FullScreenPanelIcon, 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, 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 { ExternalContourActionsMenu } from "./actions-menu"; import { ExternalContourStatePill } from "./state-pill"; import { ExternalContourDeclineModal } from "./decline-modal"; import { ExternalContourSubscriptionButton, useExternalContourSubscription, } 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; sourceProjectId: string; 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) { const { workspaceSlug, sourceProjectId, contourRequest, hasDirectTargetAccess, isSubmitting, removeRoutePeekId, peekMode, setPeekMode, currentTab, embedIssue = false, } = props; const { t } = useTranslation(); const router = useAppRouter(); const [isDeclineModalOpen, setIsDeclineModalOpen] = useState(false); const { decideRequest, filteredRequestIds, handleCurrentTab, loader } = useProjectExternalContours(); const { currentTab: boardCurrentTab, columnIdsMap } = useProjectExternalContoursBoard(); const { getProjectById } = useProject(); const issue = contourRequest.issue; const currentRequestId = contourRequest.id; 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"); const isSourceAccepted = contourRequest.source_decision === "accepted"; const redirectToRelativeIssue = useCallback( (direction: "next" | "prev") => { 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) % 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, hasRelativeNavigation, relativeRequestIds, router, sourceProjectId, workspaceSlug] ); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.key === "ArrowUp") redirectToRelativeIssue("prev"); if (event.key === "ArrowDown") redirectToRelativeIssue("next"); }; document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }, [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, issueId: issue.id, projectIdentifier: targetProjectIdentifier, sequenceId: issue.sequence_id, }); const subscriptionProjectId = issue.project_id || sourceProjectId; const { isSubscribed, loading: isSubscriptionLoading, toggleSubscription } = useExternalContourSubscription({ workspaceSlug, projectId: subscriptionProjectId, issueId: issue.id, }); const handleCopyLink = () => copyUrlToClipboard(requestLink).then(() => setToast({ type: TOAST_TYPE.SUCCESS, title: t("common.link_copied"), message: t("common.copied_to_clipboard"), }) ); const handleToggleSubscription = async () => { try { const nextValue = !isSubscribed; await toggleSubscription(); setToast({ type: TOAST_TYPE.SUCCESS, title: t("toast.success"), message: nextValue ? t("issue.subscription.actions.subscribed") : t("issue.subscription.actions.unsubscribed"), }); } catch { setToast({ type: TOAST_TYPE.ERROR, title: t("toast.error"), message: t("common.error.message"), }); } }; const handleOpenTarget = () => { router.push(workItemLink); }; const handleDecision = async (action: "accept" | "decline", comment?: string) => { try { await decideRequest(workspaceSlug, sourceProjectId, contourRequest.id, action, comment); if (action === "decline") { setIsDeclineModalOpen(false); await handleCurrentTab(workspaceSlug, sourceProjectId, EInboxIssueCurrentTab.OPEN); router.push( `/${workspaceSlug}/projects/${sourceProjectId}/external-contours?currentTab=${EInboxIssueCurrentTab.OPEN}&inboxIssueId=${contourRequest.id}` ); } setToast({ type: TOAST_TYPE.SUCCESS, title: t("success"), message: t( action === "accept" ? "external_contours_page.actions.accept_success" : "external_contours_page.actions.decline_success" ), }); } catch (error: any) { setToast({ type: TOAST_TYPE.ERROR, title: t("error"), message: error?.error || t("external_contours_page.actions.decision_error"), }); } }; return ( <> setIsDeclineModalOpen(false)} onSubmit={(comment) => handleDecision("decline", comment)} />
{hasDirectTargetAccess && ( removeRoutePeekId()} target="_self"> )}
{isSourceAccepted && (
{t("external_contours_page.traceability.source_decision_accepted")}
)} handleDecision("accept")} onCopy={handleCopyLink} onDecline={() => setIsDeclineModalOpen(true)} onOpenTarget={handleOpenTarget} onToggleSubscription={handleToggleSubscription} />
); });