Skip to content

Commit b27ef92

Browse files
committed
Ask to delete orphaned speakers when deleting a session
After a session is deleted, check if any of its speakers are not linked to any other session. If orphaned speakers are found, show a dialog with checkboxes letting the user select which ones to delete. https://claude.ai/code/session_01SdkRECfqLSaJCouvYNPuKB
1 parent 9d07420 commit b27ef92

File tree

1 file changed

+108
-4
lines changed

1 file changed

+108
-4
lines changed

src/events/page/sessions/EventSession.tsx

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
1-
import { useState } from 'react'
2-
import { Event } from '../../../types'
3-
import { Box, Button, Card, Container, DialogContentText, Typography } from '@mui/material'
1+
import { useCallback, useState } from 'react'
2+
import { Event, Speaker } from '../../../types'
3+
import {
4+
Box,
5+
Button,
6+
Card,
7+
Checkbox,
8+
Container,
9+
DialogContentText,
10+
FormControlLabel,
11+
List,
12+
ListItem,
13+
Typography,
14+
} from '@mui/material'
415
import { useSession } from '../../../services/hooks/useSession'
516
import { useLocation, useRoute } from 'wouter'
617
import { FirestoreQueryLoaderAndErrorDisplay } from '../../../components/FirestoreQueryLoaderAndErrorDisplay'
718
import { ArrowBack } from '@mui/icons-material'
819
import { getQueryParams } from '../../../utils/getQuerySearchParameters'
920
import { EventSessionForm } from './EventSessionForm'
10-
import { doc } from 'firebase/firestore'
21+
import { deleteDoc, doc, getDocs, query, where } from 'firebase/firestore'
1122
import { collections } from '../../../services/firebase'
1223
import { ConfirmDialog } from '../../../components/ConfirmDialog'
1324
import {
1425
useFirestoreDocumentDeletion,
1526
useFirestoreDocumentMutation,
1627
} from '../../../services/hooks/firestoreMutationHooks'
28+
import { useSpeakersMap } from '../../../services/hooks/useSpeakersMap'
1729

1830
export type EventSessionProps = {
1931
event: Event
@@ -24,10 +36,49 @@ export const EventSession = ({ event }: EventSessionProps) => {
2436

2537
const sessionId = params?.sessionId || ''
2638
const sessionResult = useSession(event.id, sessionId)
39+
const speakersMap = useSpeakersMap(event.id)
2740
const [deleteOpen, setDeleteOpen] = useState(false)
41+
const [orphanedSpeakers, setOrphanedSpeakers] = useState<Speaker[]>([])
42+
const [orphanDeleteOpen, setOrphanDeleteOpen] = useState(false)
43+
const [orphanDeleting, setOrphanDeleting] = useState(false)
44+
const [selectedOrphanIds, setSelectedOrphanIds] = useState<Set<string>>(new Set())
2845
const documentDeletion = useFirestoreDocumentDeletion(doc(collections.sessions(event.id), sessionId))
2946
const mutation = useFirestoreDocumentMutation(doc(collections.sessions(event.id), sessionId))
3047

48+
const findOrphanedSpeakers = useCallback(
49+
async (speakerIds: string[]) => {
50+
const orphaned: Speaker[] = []
51+
for (const speakerId of speakerIds) {
52+
const sessionsQuery = query(
53+
collections.sessions(event.id),
54+
where('speakers', 'array-contains', speakerId)
55+
)
56+
const snapshot = await getDocs(sessionsQuery)
57+
if (snapshot.empty) {
58+
const speaker = speakersMap.data?.[speakerId]
59+
if (speaker) {
60+
orphaned.push(speaker)
61+
}
62+
}
63+
}
64+
return orphaned
65+
},
66+
[event.id, speakersMap.data]
67+
)
68+
69+
const deleteSelectedOrphans = useCallback(async () => {
70+
setOrphanDeleting(true)
71+
try {
72+
for (const speakerId of selectedOrphanIds) {
73+
await deleteDoc(doc(collections.speakers(event.id), speakerId))
74+
}
75+
} finally {
76+
setOrphanDeleting(false)
77+
setOrphanDeleteOpen(false)
78+
setLocation('/sessions')
79+
}
80+
}, [event.id, selectedOrphanIds])
81+
3182
if (sessionResult.isLoading || !sessionResult.data) {
3283
return <FirestoreQueryLoaderAndErrorDisplay hookResult={sessionResult} />
3384
}
@@ -73,15 +124,68 @@ export const EventSession = ({ event }: EventSessionProps) => {
73124
cancelButton="cancel"
74125
handleClose={() => setDeleteOpen(false)}
75126
handleAccept={async () => {
127+
const speakerIds = session.speakers || []
76128
await documentDeletion.mutate()
77129
setDeleteOpen(false)
130+
131+
if (speakerIds.length > 0) {
132+
const orphaned = await findOrphanedSpeakers(speakerIds)
133+
if (orphaned.length > 0) {
134+
setOrphanedSpeakers(orphaned)
135+
setSelectedOrphanIds(new Set(orphaned.map((s) => s.id)))
136+
setOrphanDeleteOpen(true)
137+
return
138+
}
139+
}
78140
setLocation('/sessions')
79141
}}>
80142
<DialogContentText id="alert-dialog-description">
81143
{' '}
82144
Delete the session {session.title} from this event (not the session's speaker(s))
83145
</DialogContentText>
84146
</ConfirmDialog>
147+
148+
<ConfirmDialog
149+
open={orphanDeleteOpen}
150+
title="Delete orphaned speaker(s)?"
151+
acceptButton="Delete selected speaker(s)"
152+
disabled={orphanDeleting || selectedOrphanIds.size === 0}
153+
loading={orphanDeleting}
154+
cancelButton="Keep all"
155+
handleClose={() => {
156+
setOrphanDeleteOpen(false)
157+
setLocation('/sessions')
158+
}}
159+
handleAccept={deleteSelectedOrphans}>
160+
<DialogContentText>
161+
The following speaker(s) are not linked to any other session. Do you want to delete them?
162+
</DialogContentText>
163+
<List dense>
164+
{orphanedSpeakers.map((speaker) => (
165+
<ListItem key={speaker.id} disablePadding>
166+
<FormControlLabel
167+
control={
168+
<Checkbox
169+
checked={selectedOrphanIds.has(speaker.id)}
170+
onChange={(e) => {
171+
setSelectedOrphanIds((prev) => {
172+
const next = new Set(prev)
173+
if (e.target.checked) {
174+
next.add(speaker.id)
175+
} else {
176+
next.delete(speaker.id)
177+
}
178+
return next
179+
})
180+
}}
181+
/>
182+
}
183+
label={speaker.name}
184+
/>
185+
</ListItem>
186+
))}
187+
</List>
188+
</ConfirmDialog>
85189
</Container>
86190
)
87191
}

0 commit comments

Comments
 (0)