diff --git a/client/src/pages/meeting-detail/header.test.tsx b/client/src/pages/meeting-detail/header.test.tsx
index efd0363..259c53e 100644
--- a/client/src/pages/meeting-detail/header.test.tsx
+++ b/client/src/pages/meeting-detail/header.test.tsx
@@ -21,11 +21,13 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuItem: ({
children,
onClick,
+ disabled,
}: {
children: React.ReactNode;
onClick?: () => void;
+ disabled?: boolean;
}) => (
-
+
+
+
+
+
+
+
+
+
+ Delete Meeting
+
+
+
);
diff --git a/client/src/pages/meeting-detail/index.tsx b/client/src/pages/meeting-detail/index.tsx
index 34ba347..7c9021a 100644
--- a/client/src/pages/meeting-detail/index.tsx
+++ b/client/src/pages/meeting-detail/index.tsx
@@ -19,8 +19,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { PriorityBadge } from '@/components/common';
+import { ConfirmationDialog } from '@/components/ui/confirmation-dialog';
import { useGuardedMutation } from '@/hooks';
-import { toast } from '@/hooks/ui/use-toast';
import { toastError } from '@/lib/observability/errors';
import { buildExportBlob, downloadBlob } from '@/lib/utils/download';
@@ -33,6 +33,7 @@ import { AskPanel } from './ask-panel';
import { Header } from './header';
import { SummaryPanel } from './summary-panel';
import { MeetingTranscriptRow } from './transcript-row';
+import { useDeleteMeeting } from './use-delete-meeting';
import { useMeetingDetail } from './use-meeting-detail';
import { usePlayback } from './use-playback';
@@ -69,6 +70,14 @@ export default function MeetingDetailPage() {
handleGenerateSummary,
} = useMeetingDetail({ meetingId: id });
+ const {
+ showDeleteDialog,
+ setShowDeleteDialog,
+ isDeleting,
+ canDelete,
+ handleDelete,
+ } = useDeleteMeeting({ meeting });
+
const {
playback,
seekPosition,
@@ -126,34 +135,6 @@ export default function MeetingDetailPage() {
});
};
- const guardedPlay = async (startTime?: number) => {
- await guard(() => handlePlay(startTime), {
- title: 'Offline mode',
- message: 'Playback requires an active server connection.',
- });
- };
-
- const guardedPause = async () => {
- await guard(handlePause, {
- title: 'Offline mode',
- message: 'Playback requires an active server connection.',
- });
- };
-
- const guardedStop = async () => {
- await guard(handleStop, {
- title: 'Offline mode',
- message: 'Playback requires an active server connection.',
- });
- };
-
- const guardedSeek = async (value: number) => {
- await guard(() => handleSeek(value), {
- title: 'Offline mode',
- message: 'Playback requires an active server connection.',
- });
- };
-
const handleCitationClick = (citation: SegmentCitation) => {
const segmentIndex = meeting?.segments.findIndex(
(s) => s.segment_id === citation.segment_id
@@ -208,27 +189,27 @@ export default function MeetingDetailPage() {
status: 'open',
});
- toast({
+ toastError({
title: 'Added to tasks',
- description: 'Action item is now in your open tasks.',
+ message: 'Action item is now in your open tasks.',
});
return created;
}
if (match.task.status === 'open') {
- toast({
+ toastError({
title: 'Already in tasks',
- description: 'This action item is already in your open tasks.',
+ message: 'This action item is already in your open tasks.',
});
return match;
}
await getAPI().updateTask({ task_id: match.task.id, status: 'open' });
- toast({
+ toastError({
title: 'Added to tasks',
- description: 'Action item is now in your open tasks.',
+ message: 'Action item is now in your open tasks.',
});
return match;
@@ -278,13 +259,27 @@ export default function MeetingDetailPage() {
setSeekPosition={setSeekPosition}
isExtracting={isExtracting}
onNavigateBack={() => navigate(-1)}
- onPlay={guardedPlay}
- onPause={guardedPause}
- onStop={guardedStop}
- onSeek={guardedSeek}
+ onPlay={(startTime) => guard(() => handlePlay(startTime), { title: 'Offline mode', message: 'Playback requires an active server connection.' })}
+ onPause={() => guard(handlePause, { title: 'Offline mode', message: 'Playback requires an active server connection.' })}
+ onStop={() => guard(handleStop, { title: 'Offline mode', message: 'Playback requires an active server connection.' })}
+ onSeek={(value) => guard(() => handleSeek(value), { title: 'Offline mode', message: 'Playback requires an active server connection.' })}
onExport={handleExport}
onGenerateSummary={guardedGenerateSummary}
onExtractEntities={extractEntities}
+ onDelete={() => setShowDeleteDialog(true)}
+ isDeleting={isDeleting}
+ canDelete={canDelete}
+ />
+
+
{processingState.isActive && (
diff --git a/client/src/pages/meeting-detail/use-delete-meeting.ts b/client/src/pages/meeting-detail/use-delete-meeting.ts
new file mode 100644
index 0000000..1342be5
--- /dev/null
+++ b/client/src/pages/meeting-detail/use-delete-meeting.ts
@@ -0,0 +1,64 @@
+/**
+ * Hook for managing meeting deletion flow with confirmation dialog.
+ */
+
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import type { Meeting } from '@/api/types';
+import { useGuardedMutation } from '@/hooks';
+import { useDeleteMeeting as useDeleteMeetingMutation } from '@/hooks/meetings/use-meeting-mutations';
+import { toast } from '@/hooks/ui/use-toast';
+
+interface UseDeleteMeetingOptions {
+ meeting: Meeting | undefined;
+}
+
+interface UseDeleteMeetingResult {
+ showDeleteDialog: boolean;
+ setShowDeleteDialog: (show: boolean) => void;
+ isDeleting: boolean;
+ canDelete: boolean;
+ handleDelete: () => Promise;
+}
+
+export function useDeleteMeeting({ meeting }: UseDeleteMeetingOptions): UseDeleteMeetingResult {
+ const navigate = useNavigate();
+ const { guard } = useGuardedMutation();
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+ const { mutate: deleteMeeting, isLoading: isDeleting } = useDeleteMeetingMutation();
+
+ const canDelete = meeting?.state !== 'recording' && meeting?.state !== 'stopping';
+
+ const handleDelete = async () => {
+ if (!meeting) return;
+
+ const result = await guard(
+ async () => {
+ await deleteMeeting(meeting.id);
+ return true;
+ },
+ {
+ title: 'Offline mode',
+ message: 'Deleting meetings requires an active server connection.',
+ }
+ );
+
+ if (result) {
+ setShowDeleteDialog(false);
+ toast({
+ title: 'Meeting deleted',
+ description: `"${meeting.title}" has been deleted.`,
+ });
+ navigate('/meetings');
+ }
+ };
+
+ return {
+ showDeleteDialog,
+ setShowDeleteDialog,
+ isDeleting,
+ canDelete,
+ handleDelete,
+ };
+}