diff --git a/plane-src/apps/web/ce/components/projects/external-contours/actions-menu.tsx b/plane-src/apps/web/ce/components/projects/external-contours/actions-menu.tsx new file mode 100644 index 0000000..2cf098b --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/actions-menu.tsx @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { MoreHorizontal, Bell, BellOff } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; +import { CheckCircleFilledIcon, CloseCircleFilledIcon, CopyLinkIcon, NewTabIcon } from "@plane/propel/icons"; +import { CustomMenu } from "@plane/ui"; + +type Props = { + canOpenTargetWorkItem: boolean; + canReviewClosedRequest: boolean; + includeDecisionActions?: boolean; + isSubscribed?: boolean; + isSubscriptionLoading?: boolean; + onAccept?: () => void; + onCopy: () => void; + onDecline?: () => void; + onOpenTarget?: () => void; + onToggleSubscription?: () => void; +}; + +export const ExternalContourActionsMenu = (props: Props) => { + const { + canOpenTargetWorkItem, + canReviewClosedRequest, + includeDecisionActions = false, + isSubscribed, + isSubscriptionLoading = false, + onAccept, + onCopy, + onDecline, + onOpenTarget, + onToggleSubscription, + } = props; + const { t } = useTranslation(); + + return ( + } + customButtonClassName={getIconButtonStyling("secondary", "lg")} + placement="bottom-start" + > + {includeDecisionActions && canReviewClosedRequest && onAccept && ( + +
+ + {t("external_contours_page.actions.accept")} +
+
+ )} + + {includeDecisionActions && canReviewClosedRequest && onDecline && ( + +
+ + {t("external_contours_page.actions.decline")} +
+
+ )} + + +
+ + {t("external_contours_page.actions.copy")} +
+
+ + {canOpenTargetWorkItem && onOpenTarget && ( + +
+ + {t("external_contours_page.actions.open")} +
+
+ )} + + {canOpenTargetWorkItem && onToggleSubscription && ( + +
+ {isSubscribed ? : } + {isSubscribed ? t("common.actions.unsubscribe") : t("common.actions.subscribe")} +
+
+ )} +
+ ); +}; 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 eb19e60..1f99436 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,12 +4,12 @@ * See the LICENSE file for details. */ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { MoreHorizontal, MoveDiagonal, MoveRight } from "lucide-react"; +import { MoveDiagonal, MoveRight } from "lucide-react"; import { useTranslation } from "@plane/i18n"; -import { IconButton, getIconButtonStyling } from "@plane/propel/icon-button"; +import { IconButton } from "@plane/propel/icon-button"; import { Button } from "@plane/propel/button"; import { CenterPanelIcon, @@ -19,22 +19,25 @@ import { 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, CustomMenu, CustomSelect, Header, Row, Tooltip } from "@plane/ui"; +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 { ExternalContourSubscription } from "./subscription"; +import { + ExternalContourSubscriptionButton, + useExternalContourSubscription, +} from "./subscription"; export type TExternalContourPeekMode = "side-peek" | "modal" | "full-screen"; @@ -82,7 +85,6 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont currentTab, embedIssue = false, } = props; - const parentRef = useRef(null); const { t } = useTranslation(); const router = useAppRouter(); const [isDeclineModalOpen, setIsDeclineModalOpen] = useState(false); @@ -138,6 +140,12 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont 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(() => @@ -148,6 +156,28 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont }) ); + 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); @@ -186,7 +216,6 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont /> @@ -339,63 +357,25 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont )} - {currentMode && !embedIssue && ( - setPeekMode(value)} - customButton={} - > - {PEEK_OPTIONS.map((mode) => ( - -
- - {t(mode.i18n_title)} -
-
- ))} -
- )}
- {canReviewClosedRequest && ( - <> - - - - )} {isSourceAccepted && (
{t("external_contours_page.traceability.source_decision_accepted")}
)} - } - customButtonClassName={getIconButtonStyling("secondary", "lg")} - placement="bottom-start" - > - -
- - {t("external_contours_page.actions.copy")} -
-
- {hasDirectTargetAccess && ( - router.push(workItemLink)}> -
- - {t("external_contours_page.actions.open")} -
-
- )} -
+ handleDecision("accept")} + onCopy={handleCopyLink} + onDecline={() => setIsDeclineModalOpen(true)} + onOpenTarget={handleOpenTarget} + onToggleSubscription={handleToggleSubscription} + />
diff --git a/plane-src/apps/web/ce/components/projects/external-contours/subscription.tsx b/plane-src/apps/web/ce/components/projects/external-contours/subscription.tsx index d3f916f..7688077 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/subscription.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/subscription.tsx @@ -16,16 +16,17 @@ import { IssueService } from "@/services/issue/issue.service"; const issueService = new IssueService(); -type Props = { +type TSubscriptionIdentity = { workspaceSlug: string; projectId: string; issueId: string; +}; + +type Props = TSubscriptionIdentity & { buttonClassName?: string; }; -export const ExternalContourSubscription = observer(function ExternalContourSubscription(props: Props) { - const { workspaceSlug, projectId, issueId, buttonClassName } = props; - const { t } = useTranslation(); +export const useExternalContourSubscription = ({ workspaceSlug, projectId, issueId }: TSubscriptionIdentity) => { const [isSubscribed, setIsSubscribed] = useState(undefined); const [loading, setLoading] = useState(false); @@ -50,7 +51,7 @@ export const ExternalContourSubscription = observer(function ExternalContourSubs }; }, [workspaceSlug, projectId, issueId]); - const handleSubscription = async () => { + const toggleSubscription = async () => { if (!workspaceSlug || !projectId || !issueId) return; const nextValue = !isSubscribed; @@ -63,24 +64,34 @@ export const ExternalContourSubscription = observer(function ExternalContourSubs } else { await issueService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId); } - - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("toast.success"), - message: nextValue ? t("issue.subscription.actions.subscribed") : t("issue.subscription.actions.unsubscribed"), - }); } catch { setIsSubscribed(!nextValue); - setToast({ - type: TOAST_TYPE.ERROR, - title: t("toast.error"), - message: t("common.error.message"), - }); + throw new Error("subscription-toggle-failed"); } finally { setLoading(false); } }; + return { + isSubscribed, + loading, + toggleSubscription, + }; +}; + +type TExternalContourSubscriptionButtonProps = { + isSubscribed: boolean | undefined; + loading: boolean; + onToggle: () => Promise; + buttonClassName?: string; +}; + +export const ExternalContourSubscriptionButton = observer(function ExternalContourSubscriptionButton( + props: TExternalContourSubscriptionButtonProps +) { + const { isSubscribed, loading, onToggle, buttonClassName } = props; + const { t } = useTranslation(); + if (isSubscribed === undefined) { return ( @@ -94,7 +105,7 @@ export const ExternalContourSubscription = observer(function ExternalContourSubs prependIcon={isSubscribed ? : } variant="secondary" className={cn("hover:!bg-accent-primary/20", buttonClassName)} - onClick={handleSubscription} + onClick={() => void onToggle()} disabled={loading} size="lg" > @@ -108,3 +119,40 @@ export const ExternalContourSubscription = observer(function ExternalContourSubs ); }); + +export const ExternalContourSubscription = observer(function ExternalContourSubscription(props: Props) { + const { workspaceSlug, projectId, issueId, buttonClassName } = props; + const { t } = useTranslation(); + const { isSubscribed, loading, toggleSubscription } = useExternalContourSubscription({ + workspaceSlug, + projectId, + issueId, + }); + + const handleToggle = 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"), + }); + } + }; + + return ( + + ); +});