Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/frontend/src/components/common/PlanCancellationDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import {
Dialog,
DialogTrigger,
DialogSurface,
DialogTitle,
DialogContent,
DialogBody,
DialogActions,
Button,
} from '@fluentui/react-components';
import { Warning20Regular } from '@fluentui/react-icons';
import "../../styles/Panel.css";

interface PlanCancellationDialogProps {
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
loading?: boolean;
}

/**
* Confirmation dialog for plan cancellation when navigating during active plans
*/
const PlanCancellationDialog: React.FC<PlanCancellationDialogProps> = ({
isOpen,
onConfirm,
onCancel,
loading = false
}) => {
return (
<Dialog open={isOpen} onOpenChange={(_, data) => !data.open && onCancel()}>
<DialogSurface>
<DialogBody>
<DialogTitle>
<div className="plan-cancellation-dialog-title">
<Warning20Regular className="plan-cancellation-warning-icon" />
Confirm Plan Cancellation
</div>
</DialogTitle>
<DialogContent>
If you continue, the plan process will be stopped and the plan will be cancelled.
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button
appearance="secondary"
onClick={onCancel}
disabled={loading}
>
Cancel
</Button>
</DialogTrigger>
<Button
appearance="primary"
onClick={onConfirm}
disabled={loading}
>
{loading ? 'Cancelling...' : 'Yes'}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
};

export default PlanCancellationDialog;
49 changes: 36 additions & 13 deletions src/frontend/src/components/content/PlanPanelLeft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ import TeamService from "@/services/TeamService";

const PlanPanelLeft: React.FC<PlanPanelLefProps> = ({
reloadTasks,
onNewTaskButton,
restReload,
onTeamSelect,
onTeamUpload,
isHomePage,
selectedTeam: parentSelectedTeam
selectedTeam: parentSelectedTeam,
onNavigationWithAlert
}) => {
const { dispatchToast } = useToastController("toast");
const navigate = useNavigate();
Expand Down Expand Up @@ -116,16 +118,36 @@ const PlanPanelLeft: React.FC<PlanPanelLefProps> = ({

const handleTaskSelect = useCallback(
(taskId: string) => {
const selectedPlan = plans?.find(
(plan: Plan) => plan.session_id === taskId
);
if (selectedPlan) {
navigate(`/plan/${selectedPlan.id}`);
const performNavigation = () => {
const selectedPlan = plans?.find(
(plan: Plan) => plan.session_id === taskId
);
if (selectedPlan) {
navigate(`/plan/${selectedPlan.id}`);
}
};

if (onNavigationWithAlert) {
onNavigationWithAlert(performNavigation);
} else {
performNavigation();
}
},
[plans, navigate]
[plans, navigate, onNavigationWithAlert]
);

const handleLogoClick = useCallback(() => {
const performNavigation = () => {
navigate("/");
};

if (onNavigationWithAlert) {
onNavigationWithAlert(performNavigation);
} else {
performNavigation();
}
}, [navigate, onNavigationWithAlert]);

const handleTeamSelect = useCallback(
(team: TeamConfig | null) => {
// Use parent's team select handler if provided, otherwise use local state
Expand Down Expand Up @@ -163,10 +185,11 @@ const PlanPanelLeft: React.FC<PlanPanelLefProps> = ({
);

return (
<div style={{ flexShrink: 0, display: "flex", overflow: "hidden" }}>
<div className="panel-left-container">
<PanelLeft panelWidth={280} panelResize={true}>
<PanelLeftToolbar
linkTo="/"
linkTo={onNavigationWithAlert ? undefined : "/"}
onTitleClick={onNavigationWithAlert ? handleLogoClick : undefined}
panelTitle="Contoso"
panelIcon={<ContosoLogo />}
>
Expand All @@ -175,7 +198,7 @@ const PlanPanelLeft: React.FC<PlanPanelLefProps> = ({

{/* Team Selector right under the toolbar */}

<div style={{ marginTop: '8px', marginBottom: '8px' }}>
<div className="team-selector-container">
{isHomePage && (
<TeamSelector
onTeamSelect={handleTeamSelect}
Expand All @@ -194,13 +217,13 @@ const PlanPanelLeft: React.FC<PlanPanelLefProps> = ({
</div>
<div
className="tab tab-new-task"
onClick={() => navigate("/", { state: { focusInput: true } })}
onClick={onNewTaskButton}
tabIndex={0} // ✅ allows tab focus
role="button" // ✅ announces as button
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
navigate("/", { state: { focusInput: true } });
onNewTaskButton();
}
}}
>
Expand All @@ -219,7 +242,7 @@ const PlanPanelLeft: React.FC<PlanPanelLefProps> = ({
/>

<PanelFooter>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', width: '100%' }}>
<div className="panel-footer-content">
{/* User Card */}
<PanelUserCard
name={userInfo?.user_first_last_name || "Guest"}
Expand Down
80 changes: 25 additions & 55 deletions src/frontend/src/coral/components/Panels/PanelLeftToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,67 @@
import React, { ReactNode } from "react";
import { Subtitle2 } from "@fluentui/react-components";
import { Link } from "react-router-dom";
import "../../../styles/Panel.css";

interface PanelLeftToolbarProps {
panelIcon?: ReactNode;
panelTitle?: string | null;
linkTo?: string;
onTitleClick?: () => void;
children?: ReactNode;
}

const PanelLeftToolbar: React.FC<PanelLeftToolbarProps> = ({
panelIcon,
panelTitle,
linkTo,
onTitleClick,
children,
}) => {
const TitleContent = (
<div
className="panelTitle"
style={{
display: "flex",
alignItems: "center",
gap: "6px",
flexShrink: 1,
overflow: "hidden",
minWidth: 0,
}}
>
<div className="panel-title">
{panelIcon && (
<div
style={{
flexShrink: 0,
display: "flex",
alignItems: "center",
}}
>
<div className="panel-title-icon">
{panelIcon}
</div>
)}
{panelTitle && (
<Subtitle2
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
<Subtitle2 className="panel-title-text">
{panelTitle}
</Subtitle2>
)}
</div>
);

return (
<div
className="panelToolbar"
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "16px",
boxSizing: "border-box",
height: "56px",
}}
>
<div className="panel-toolbar">
{(panelIcon || panelTitle) &&
(linkTo ? (
(onTitleClick ? (
<div
onClick={onTitleClick}
className="panel-title-clickable clickable-element"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onTitleClick();
}
}}
>
{TitleContent}
</div>
) : linkTo ? (
<Link
to={linkTo}
style={{
textDecoration: "none",
color: "inherit",
display: "flex",
alignItems: "center",
minWidth: 0,
flexShrink: 1,
}}
className="panel-title-clickable"
>
{TitleContent}
</Link>
) : (
TitleContent
))}
<div
className="panelTools"
style={{
display: "flex",
alignItems: "center",
flexGrow: 1,
justifyContent: "flex-end",
minWidth: 0,
}}
>
<div className="panel-tools">
{children}
</div>
</div>
Expand Down
75 changes: 75 additions & 0 deletions src/frontend/src/hooks/usePlanCancellationAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useCallback } from 'react';
import { PlanStatus } from '../models';
import { APIService } from '../api/apiService';

interface UsePlanCancellationAlertProps {
planData: any;
planApprovalRequest: any;
onNavigate: () => void;
}

/**
* Custom hook to handle plan cancellation alerts when navigating during active plans
*/
export const usePlanCancellationAlert = ({
planData,
planApprovalRequest,
onNavigate
}: UsePlanCancellationAlertProps) => {
const apiService = new APIService();

/**
* Check if a plan is currently active/running
*/
const isPlanActive = useCallback(() => {
return planData?.plan?.overall_status === PlanStatus.IN_PROGRESS;
}, [planData]);

/**
* Handle the confirmation dialog and plan cancellation
*/
const handleNavigationWithConfirmation = useCallback(async () => {
if (!isPlanActive()) {
// Plan is not active, proceed with navigation
onNavigate();
return;
}

// Show confirmation dialog
const userConfirmed = window.confirm(
"If you continue, the plan process will be stopped and the plan will be cancelled."
);

if (!userConfirmed) {
// User cancelled, do nothing
return;
}

try {
// User confirmed, cancel the plan
if (planApprovalRequest?.id) {
await apiService.approvePlan({
m_plan_id: planApprovalRequest.id,
plan_id: planData?.plan?.id,
approved: false,
feedback: 'Plan cancelled by user navigation'
});
}

// Navigate after successful cancellation
onNavigate();
} catch (error) {
console.error('❌ Failed to cancel plan:', error);
// Show error but still allow navigation
alert('Failed to cancel the plan properly, but navigation will continue.');
onNavigate();
}
}, [isPlanActive, onNavigate, planApprovalRequest, planData, apiService]);

return {
isPlanActive,
handleNavigationWithConfirmation
};
};

export default usePlanCancellationAlert;
1 change: 1 addition & 0 deletions src/frontend/src/models/planPanelLeft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface PlanPanelLefProps {
onTeamUpload?: () => Promise<void>;
isHomePage: boolean;
selectedTeam?: TeamConfig | null;
onNavigationWithAlert?: (navigationFn: () => void) => void;
}
Loading
Loading