feat(client): integrate bulk delete in Meetings page

- Add selection state management with Set<string>
- Integrate useDeleteMeetings hook with confirmation dialog
- Implement select/deselect/selectAll handlers
- Render BulkActionToolbar when selections > 0
- Clear selections on filter/pagination changes
- Add ConfirmationDialog for bulk delete confirmation
- Fix missing index.ts for request types
- Fix useToast import path

Completes full bulk delete flow from UI to backend.

Refs: mass-delete-meetings plan task 8
This commit is contained in:
2026-01-26 10:27:47 +00:00
parent 2ac921da1f
commit b9eee07135
3 changed files with 89 additions and 2 deletions

View File

@@ -0,0 +1,11 @@
export * from './ai';
export * from './annotations';
export * from './assistant';
export * from './audio';
export * from './integrations';
export * from './meetings';
export * from './oidc';
export * from './preferences';
export * from './recording-apps';
export * from './templates';
export * from './triggers';

View File

@@ -4,7 +4,7 @@ import { meetingCache } from '@/lib/cache/meeting-cache';
import { getAPI } from '@/api/interface';
import type { Meeting } from '@/api/types';
import type { CreateMeetingRequest, DeleteMeetingsResult } from '@/api/types/requests/meetings';
import { useToast } from '@/hooks/use-toast';
import { useToast } from '@/hooks/ui/use-toast';
import { useQueryClient } from '@tanstack/react-query';
interface CreateMeetingContext {

View File

@@ -7,15 +7,17 @@ import { getAPI } from '@/api/interface';
import type { Meeting, MeetingState } from '@/api/types';
import type { ProjectScope } from '@/api/types/requests';
import { EmptyState } from '@/components/common';
import { MeetingCard } from '@/components/features/meetings';
import { BulkActionToolbar, MeetingCard } from '@/components/features/meetings';
import { ProjectScopeFilter } from '@/components/features/projects/ProjectScopeFilter';
import { Button } from '@/components/ui/button';
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog';
import { Input } from '@/components/ui/input';
import { SearchIcon } from '@/components/ui/search-icon';
import { SkeletonMeetingCard } from '@/components/ui/skeleton';
import { UpcomingMeetings } from '@/components/features/calendar';
import { useProjects } from '@/contexts/project-state';
import { useGuardedMutation } from '@/hooks';
import { useDeleteMeetings } from '@/hooks/meetings/use-meeting-mutations';
import { addClientLog } from '@/lib/observability/client';
import { preferences } from '@/lib/preferences';
import { MEETINGS_PAGE_LIMIT, SKELETON_CARDS_COUNT } from '@/lib/constants/timing';
@@ -49,6 +51,11 @@ export default function MeetingsPage() {
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
// Bulk selection state
const [selectedMeetingIds, setSelectedMeetingIds] = useState<Set<string>>(new Set());
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
const { mutate: deleteMeetings, isLoading: isDeleting } = useDeleteMeetings();
const shouldSkipFetch =
(projectScope === 'selected' && selectedProjectIds.length === 0) ||
(projectScope === 'active' && !resolvedProjectId && !projectsLoading);
@@ -107,11 +114,57 @@ export default function MeetingsPage() {
preferences.setMeetingsProjectFilter(projectScope, selectedProjectIds);
}, [projectScope, selectedProjectIds]);
// Clear selections when filters change
useEffect(() => {
setSelectedMeetingIds(new Set());
}, [searchQuery, stateFilter, projectScope, resolvedProjectId]);
const filteredMeetings = useMemo(
() => meetings.filter((m) => m.title.toLowerCase().includes(searchQuery.toLowerCase())),
[meetings, searchQuery]
);
const deletableMeetings = useMemo(
() => meetings.filter((m) => m.state !== 'recording'),
[meetings]
);
const deletableMeetingIds = useMemo(
() => new Set(deletableMeetings.map((m) => m.id)),
[deletableMeetings]
);
const handleSelect = useCallback((meetingId: string, selected: boolean) => {
setSelectedMeetingIds((prev) => {
const next = new Set(prev);
if (selected) next.add(meetingId);
else next.delete(meetingId);
return next;
});
}, []);
const handleSelectAll = useCallback(() => {
setSelectedMeetingIds(new Set(deletableMeetingIds));
}, [deletableMeetingIds]);
const handleDeselectAll = useCallback(() => {
setSelectedMeetingIds(new Set());
}, []);
const handleBulkDelete = useCallback(() => {
setShowBulkDeleteDialog(true);
}, []);
const confirmBulkDelete = useCallback(() => {
deleteMeetings(Array.from(selectedMeetingIds), {
onSuccess: () => {
setSelectedMeetingIds(new Set());
setShowBulkDeleteDialog(false);
fetchMeetings(0, false);
},
});
}, [deleteMeetings, selectedMeetingIds, fetchMeetings]);
const hasMore = meetings.length < totalCount;
const handleLoadMore = () => {
@@ -209,6 +262,9 @@ export default function MeetingsPage() {
index={i}
onDelete={handleDelete}
showDeleteButton
isSelectable={meeting.state !== 'recording'}
isSelected={selectedMeetingIds.has(meeting.id)}
onSelect={handleSelect}
/>
))}
</div>
@@ -234,6 +290,26 @@ export default function MeetingsPage() {
</>
)}
</div>
<BulkActionToolbar
selectedCount={selectedMeetingIds.size}
totalCount={deletableMeetings.length}
isDeleting={isDeleting}
onSelectAll={handleSelectAll}
onDeselectAll={handleDeselectAll}
onDelete={handleBulkDelete}
/>
<ConfirmationDialog
open={showBulkDeleteDialog}
onOpenChange={setShowBulkDeleteDialog}
onConfirm={confirmBulkDelete}
title="Delete Meetings"
description={`Are you sure you want to delete ${selectedMeetingIds.size} meeting(s)? This action cannot be undone.`}
confirmText="Delete"
variant="destructive"
isLoading={isDeleting}
/>
</div>
);
}