DeleteConfirmationDialogs.tsx354 lines · main
| 1 | import { ExternalLink } from 'lucide-react' |
| 2 | import Link from 'next/link' |
| 3 | import { toast } from 'sonner' |
| 4 | import { Alert, AlertDescription, AlertTitle, Button, Checkbox } from 'ui' |
| 5 | import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' |
| 6 | |
| 7 | import { useTableFilter } from '@/components/grid/hooks/useTableFilter' |
| 8 | import type { SupaRow } from '@/components/grid/types' |
| 9 | import { useDatabaseColumnDeleteMutation } from '@/data/database-columns/database-column-delete-mutation' |
| 10 | import { TableLike } from '@/data/table-editor/table-editor-types' |
| 11 | import { useTableRowDeleteAllMutation } from '@/data/table-rows/table-row-delete-all-mutation' |
| 12 | import { useTableRowDeleteMutation } from '@/data/table-rows/table-row-delete-mutation' |
| 13 | import { useTableRowTruncateMutation } from '@/data/table-rows/table-row-truncate-mutation' |
| 14 | import { useTableDeleteMutation } from '@/data/tables/table-delete-mutation' |
| 15 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 16 | import { useGetImpersonatedRoleState } from '@/state/role-impersonation-state' |
| 17 | import { useTableEditorStateSnapshot } from '@/state/table-editor' |
| 18 | |
| 19 | export type DeleteConfirmationDialogsProps = { |
| 20 | selectedTable?: TableLike |
| 21 | onTableDeleted?: () => void |
| 22 | } |
| 23 | |
| 24 | const DeleteConfirmationDialogs = ({ |
| 25 | selectedTable, |
| 26 | onTableDeleted, |
| 27 | }: DeleteConfirmationDialogsProps) => { |
| 28 | const { data: project } = useSelectedProjectQuery() |
| 29 | const snap = useTableEditorStateSnapshot() |
| 30 | const { filters, setFilters } = useTableFilter() |
| 31 | |
| 32 | const removeDeletedColumnFromFiltersAndSorts = ({ |
| 33 | columnName, |
| 34 | }: { |
| 35 | ref?: string |
| 36 | tableName?: string |
| 37 | schema?: string |
| 38 | columnName: string |
| 39 | }) => { |
| 40 | setFilters(filters.filter((filter) => filter.column !== columnName)) |
| 41 | } |
| 42 | |
| 43 | const { mutate: deleteColumn } = useDatabaseColumnDeleteMutation({ |
| 44 | onSuccess: () => { |
| 45 | if (!(snap.confirmationDialog?.type === 'column')) return |
| 46 | const selectedColumnToDelete = snap.confirmationDialog.column |
| 47 | removeDeletedColumnFromFiltersAndSorts({ columnName: selectedColumnToDelete.name }) |
| 48 | toast.success(`Successfully deleted column "${selectedColumnToDelete.name}"`) |
| 49 | }, |
| 50 | onError: (error) => { |
| 51 | if (!(snap.confirmationDialog?.type === 'column')) return |
| 52 | const selectedColumnToDelete = snap.confirmationDialog.column |
| 53 | toast.error(`Failed to delete ${selectedColumnToDelete!.name}: ${error.message}`) |
| 54 | }, |
| 55 | onSettled: () => { |
| 56 | snap.closeConfirmationDialog() |
| 57 | }, |
| 58 | }) |
| 59 | const { mutate: deleteTable } = useTableDeleteMutation({ |
| 60 | onSuccess: async () => { |
| 61 | toast.success(`Successfully deleted table "${selectedTable?.name}"`) |
| 62 | onTableDeleted?.() |
| 63 | }, |
| 64 | onError: (error) => { |
| 65 | toast.error(`Failed to delete ${selectedTable?.name}: ${error.message}`) |
| 66 | }, |
| 67 | onSettled: () => { |
| 68 | snap.closeConfirmationDialog() |
| 69 | }, |
| 70 | }) |
| 71 | |
| 72 | const { mutate: deleteRows, isPending: isDeletingRows } = useTableRowDeleteMutation({ |
| 73 | onSuccess: () => { |
| 74 | if (snap.confirmationDialog?.type === 'row') { |
| 75 | snap.confirmationDialog.callback?.() |
| 76 | } |
| 77 | toast.success(`Successfully deleted selected row(s)`) |
| 78 | }, |
| 79 | onSettled: () => { |
| 80 | snap.closeConfirmationDialog() |
| 81 | }, |
| 82 | }) |
| 83 | |
| 84 | const { mutate: deleteAllRows, isPending: isDeletingAllRows } = useTableRowDeleteAllMutation({ |
| 85 | onSuccess: () => { |
| 86 | if (snap.confirmationDialog?.type === 'row') { |
| 87 | snap.confirmationDialog.callback?.() |
| 88 | } |
| 89 | toast.success(`Successfully deleted selected rows`) |
| 90 | }, |
| 91 | onError: (error) => { |
| 92 | toast.error(`Failed to delete rows: ${error.message}`) |
| 93 | }, |
| 94 | onSettled: () => { |
| 95 | snap.closeConfirmationDialog() |
| 96 | }, |
| 97 | }) |
| 98 | |
| 99 | const { mutate: truncateRows, isPending: isTruncatingRows } = useTableRowTruncateMutation({ |
| 100 | onSuccess: () => { |
| 101 | if (snap.confirmationDialog?.type === 'row') { |
| 102 | snap.confirmationDialog.callback?.() |
| 103 | } |
| 104 | toast.success(`Successfully deleted all rows from table`) |
| 105 | }, |
| 106 | onError: (error) => { |
| 107 | toast.error(`Failed to delete rows: ${error.message}`) |
| 108 | }, |
| 109 | onSettled: () => { |
| 110 | snap.closeConfirmationDialog() |
| 111 | }, |
| 112 | }) |
| 113 | |
| 114 | const isAllRowsSelected = |
| 115 | snap.confirmationDialog?.type === 'row' ? snap.confirmationDialog.allRowsSelected : false |
| 116 | const numRows = |
| 117 | snap.confirmationDialog?.type === 'row' |
| 118 | ? snap.confirmationDialog.allRowsSelected |
| 119 | ? (snap.confirmationDialog.numRows ?? 0) |
| 120 | : snap.confirmationDialog.rows.length |
| 121 | : 0 |
| 122 | |
| 123 | const isDeleteWithCascade = |
| 124 | snap.confirmationDialog?.type === 'column' || snap.confirmationDialog?.type === 'table' |
| 125 | ? snap.confirmationDialog.isDeleteWithCascade |
| 126 | : false |
| 127 | |
| 128 | const onConfirmDeleteColumn = async () => { |
| 129 | if (!(snap.confirmationDialog?.type === 'column')) return |
| 130 | if (project === undefined) return |
| 131 | |
| 132 | const selectedColumnToDelete = snap.confirmationDialog.column |
| 133 | if (selectedColumnToDelete === undefined) return |
| 134 | |
| 135 | deleteColumn({ |
| 136 | column: selectedColumnToDelete, |
| 137 | cascade: isDeleteWithCascade, |
| 138 | projectRef: project.ref, |
| 139 | connectionString: project?.connectionString, |
| 140 | }) |
| 141 | } |
| 142 | |
| 143 | const onConfirmDeleteTable = async () => { |
| 144 | if (!(snap.confirmationDialog?.type === 'table')) return |
| 145 | const selectedTableToDelete = selectedTable |
| 146 | |
| 147 | if (selectedTableToDelete === undefined) return |
| 148 | |
| 149 | deleteTable({ |
| 150 | projectRef: project?.ref!, |
| 151 | connectionString: project?.connectionString, |
| 152 | id: selectedTableToDelete.id, |
| 153 | name: selectedTableToDelete.name, |
| 154 | schema: selectedTableToDelete.schema, |
| 155 | cascade: isDeleteWithCascade, |
| 156 | }) |
| 157 | } |
| 158 | |
| 159 | const getImpersonatedRoleState = useGetImpersonatedRoleState() |
| 160 | |
| 161 | const onConfirmDeleteRow = async () => { |
| 162 | if (!project) return console.error('Project ref is required') |
| 163 | if (!selectedTable) return console.error('Selected table required') |
| 164 | if (snap.confirmationDialog?.type !== 'row') return |
| 165 | const selectedRowsToDelete = snap.confirmationDialog.rows |
| 166 | |
| 167 | if (snap.confirmationDialog.allRowsSelected) { |
| 168 | if (filters.length === 0) { |
| 169 | if (getImpersonatedRoleState().role !== undefined) { |
| 170 | snap.closeConfirmationDialog() |
| 171 | return toast.error('Table truncation is not supported when impersonating a role') |
| 172 | } |
| 173 | |
| 174 | truncateRows({ |
| 175 | projectRef: project.ref, |
| 176 | connectionString: project.connectionString, |
| 177 | table: selectedTable, |
| 178 | }) |
| 179 | } else { |
| 180 | deleteAllRows({ |
| 181 | projectRef: project.ref, |
| 182 | connectionString: project.connectionString, |
| 183 | table: selectedTable, |
| 184 | filters, |
| 185 | roleImpersonationState: getImpersonatedRoleState(), |
| 186 | }) |
| 187 | } |
| 188 | } else { |
| 189 | deleteRows({ |
| 190 | projectRef: project.ref, |
| 191 | connectionString: project.connectionString, |
| 192 | table: selectedTable, |
| 193 | rows: selectedRowsToDelete as SupaRow[], |
| 194 | roleImpersonationState: getImpersonatedRoleState(), |
| 195 | }) |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | return ( |
| 200 | <> |
| 201 | <ConfirmationModal |
| 202 | variant="destructive" |
| 203 | size="small" |
| 204 | visible={snap.confirmationDialog?.type === 'column'} |
| 205 | title={`Confirm deletion of column "${ |
| 206 | snap.confirmationDialog?.type === 'column' && snap.confirmationDialog.column.name |
| 207 | }"`} |
| 208 | confirmLabel="Delete" |
| 209 | confirmLabelLoading="Deleting" |
| 210 | onCancel={() => { |
| 211 | snap.closeConfirmationDialog() |
| 212 | }} |
| 213 | onConfirm={onConfirmDeleteColumn} |
| 214 | > |
| 215 | <div className="space-y-4"> |
| 216 | <p className="text-sm text-foreground-light"> |
| 217 | Are you sure you want to delete the selected column? This action cannot be undone. |
| 218 | </p> |
| 219 | <div className="items-top flex space-x-2"> |
| 220 | <Checkbox |
| 221 | id="checkbox-cascade" |
| 222 | checked={isDeleteWithCascade} |
| 223 | onCheckedChange={() => snap.toggleConfirmationIsWithCascade()} |
| 224 | /> |
| 225 | <div className="grid gap-1.5 leading-none"> |
| 226 | <label |
| 227 | htmlFor="checkbox-cascade" |
| 228 | className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" |
| 229 | > |
| 230 | Drop column with cascade? |
| 231 | </label> |
| 232 | <p className="text-sm text-foreground-muted"> |
| 233 | Deletes the column and its dependent objects |
| 234 | </p> |
| 235 | </div> |
| 236 | </div> |
| 237 | {isDeleteWithCascade && ( |
| 238 | <Alert |
| 239 | variant="warning" |
| 240 | title="Warning: Dropping with cascade may result in unintended consequences" |
| 241 | > |
| 242 | <AlertTitle> |
| 243 | All dependent objects will be removed, as will any objects that depend on them, |
| 244 | recursively. |
| 245 | </AlertTitle> |
| 246 | <AlertDescription> |
| 247 | <Button asChild size="tiny" type="default" icon={<ExternalLink />}> |
| 248 | <Link |
| 249 | href="https://www.postgresql.org/docs/current/ddl-depend.html" |
| 250 | target="_blank" |
| 251 | rel="noreferrer" |
| 252 | > |
| 253 | About dependency tracking |
| 254 | </Link> |
| 255 | </Button> |
| 256 | </AlertDescription> |
| 257 | </Alert> |
| 258 | )} |
| 259 | </div> |
| 260 | </ConfirmationModal> |
| 261 | |
| 262 | <ConfirmationModal |
| 263 | variant={'destructive'} |
| 264 | size="small" |
| 265 | visible={snap.confirmationDialog?.type === 'table'} |
| 266 | title={ |
| 267 | <span className="wrap-break-word">{`Confirm deletion of table "${selectedTable?.name}"`}</span> |
| 268 | } |
| 269 | confirmLabel="Delete" |
| 270 | confirmLabelLoading="Deleting" |
| 271 | onCancel={() => { |
| 272 | snap.closeConfirmationDialog() |
| 273 | }} |
| 274 | onConfirm={onConfirmDeleteTable} |
| 275 | > |
| 276 | <div data-testid="confirm-delete-table-modal" className="space-y-4"> |
| 277 | <p className="text-sm text-foreground-light"> |
| 278 | Are you sure you want to delete the selected table? This action cannot be undone. |
| 279 | </p> |
| 280 | <div className="items-top flex space-x-2"> |
| 281 | <Checkbox |
| 282 | id="checkbox-cascade" |
| 283 | checked={isDeleteWithCascade} |
| 284 | onCheckedChange={() => snap.toggleConfirmationIsWithCascade(!isDeleteWithCascade)} |
| 285 | /> |
| 286 | <div className="grid gap-1.5 leading-none"> |
| 287 | <label |
| 288 | htmlFor="checkbox-cascade" |
| 289 | className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" |
| 290 | > |
| 291 | Drop table with cascade? |
| 292 | </label> |
| 293 | <p className="text-sm text-foreground-muted"> |
| 294 | Deletes the table and its dependent objects |
| 295 | </p> |
| 296 | </div> |
| 297 | </div> |
| 298 | {isDeleteWithCascade && ( |
| 299 | <Alert variant="warning"> |
| 300 | <AlertTitle> |
| 301 | Warning: Dropping with cascade may result in unintended consequences |
| 302 | </AlertTitle> |
| 303 | <AlertDescription> |
| 304 | All dependent objects will be removed, as will any objects that depend on them, |
| 305 | recursively. |
| 306 | </AlertDescription> |
| 307 | <AlertDescription className="mt-4"> |
| 308 | <Button asChild size="tiny" type="default" icon={<ExternalLink />}> |
| 309 | <Link |
| 310 | href="https://www.postgresql.org/docs/current/ddl-depend.html" |
| 311 | target="_blank" |
| 312 | rel="noreferrer" |
| 313 | > |
| 314 | About dependency tracking |
| 315 | </Link> |
| 316 | </Button> |
| 317 | </AlertDescription> |
| 318 | </Alert> |
| 319 | )} |
| 320 | </div> |
| 321 | </ConfirmationModal> |
| 322 | |
| 323 | <ConfirmationModal |
| 324 | variant={'destructive'} |
| 325 | size="small" |
| 326 | visible={snap.confirmationDialog?.type === 'row'} |
| 327 | title={ |
| 328 | <p className="wrap-break-word"> |
| 329 | <span>Confirm to delete the selected row</span> |
| 330 | <span>{numRows > 1 && 's'}</span> |
| 331 | </p> |
| 332 | } |
| 333 | confirmLabel="Delete" |
| 334 | confirmLabelLoading="Deleting" |
| 335 | onCancel={() => snap.closeConfirmationDialog()} |
| 336 | onConfirm={() => onConfirmDeleteRow()} |
| 337 | loading={isTruncatingRows || isDeletingRows || isDeletingAllRows} |
| 338 | > |
| 339 | <div className="space-y-4"> |
| 340 | <p className="text-sm text-foreground-light"> |
| 341 | <span>Are you sure you want to delete </span> |
| 342 | <span>{isAllRowsSelected ? 'all' : 'the selected'} </span> |
| 343 | <span>{numRows > 1 && `${numRows} `}</span> |
| 344 | <span>row</span> |
| 345 | <span>{numRows > 1 && 's'}</span> |
| 346 | <span>? This action cannot be undone.</span> |
| 347 | </p> |
| 348 | </div> |
| 349 | </ConfirmationModal> |
| 350 | </> |
| 351 | ) |
| 352 | } |
| 353 | |
| 354 | export default DeleteConfirmationDialogs |