feat(tables): confirm view deletion (#7041)

This commit is contained in:
Justin Blumencranz
2026-08-24 12:17:59 -07:00
committed by GitHub
parent 6d313a4a94
commit 472532e1a3
2 changed files with 89 additions and 1 deletions
@@ -125,6 +125,69 @@ describe('ViewsMenu', () => {
container.remove()
})
it('confirms before deleting a saved view', () => {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
const onDelete = vi.fn()
act(() => {
root.render(
<ViewsMenu
views={[PRIMARY_VIEW, SECOND_VIEW]}
activeViewId={PRIMARY_VIEW.id}
onSelect={vi.fn()}
onRename={vi.fn()}
onSetDefault={vi.fn()}
onDelete={onDelete}
onNewView={vi.fn()}
canEdit
/>
)
})
act(() => container.querySelector<HTMLButtonElement>('button[aria-label="Views"]')?.click())
const getDeleteButton = () =>
[...document.body.querySelectorAll<HTMLButtonElement>('button[aria-label="Delete"]')].find(
(button) => button.getAttribute('aria-disabled') !== 'true'
)
const getConfirmationDialog = () =>
[...document.body.querySelectorAll<HTMLElement>('[role="dialog"]')].find((dialog) =>
dialog.textContent?.includes('This action cannot be undone.')
)
act(() => getDeleteButton()?.click())
expect(onDelete).not.toHaveBeenCalled()
const firstDialog = getConfirmationDialog()
expect(firstDialog).toHaveTextContent('Delete View')
expect(firstDialog).toHaveTextContent('Second view')
expect(firstDialog).toHaveTextContent('This action cannot be undone.')
const cancelButton = [
...(firstDialog?.querySelectorAll<HTMLButtonElement>('button') ?? []),
].find((button) => button.textContent === 'Cancel')
act(() => cancelButton?.click())
expect(getConfirmationDialog()).toBeUndefined()
expect(onDelete).not.toHaveBeenCalled()
act(() => container.querySelector<HTMLButtonElement>('button[aria-label="Views"]')?.click())
act(() => getDeleteButton()?.click())
const dialog = getConfirmationDialog()
const confirmButton = [...(dialog?.querySelectorAll<HTMLButtonElement>('button') ?? [])].find(
(button) => button.textContent === 'Delete'
)
act(() => confirmButton?.click())
expect(onDelete).toHaveBeenCalledOnce()
expect(onDelete).toHaveBeenCalledWith(SECOND_VIEW.id)
act(() => root.unmount())
container.remove()
})
it('keeps the menu open when keyboard focus moves from the trigger to the default pin', () => {
vi.useFakeTimers()
const container = document.createElement('div')
@@ -4,6 +4,7 @@ import { memo, useEffect, useRef, useState } from 'react'
import {
Button,
ChipChevronDown,
ChipConfirmModal,
chipContentLabelClass,
chipVariants,
cn,
@@ -62,9 +63,11 @@ export const ViewsMenu = memo(function ViewsMenu({
canEdit,
}: ViewsMenuProps) {
const [open, setOpen] = useState(false)
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const { activeView, defaultView } = resolveTableViewSelection(views, activeViewId)
const deleteTarget = views.find((view) => view.id === deleteTargetId)
const hasDefaultView = defaultView !== null
const label = activeView?.name ?? ALL_ROWS_VIEW_LABEL
@@ -176,7 +179,7 @@ export const ViewsMenu = memo(function ViewsMenu({
disabledReason: view.isDefault
? 'Default view cannot be deleted'
: undefined,
onClick: () => runAndClose(() => onDelete(view.id)),
onClick: () => runAndClose(() => setDeleteTargetId(view.id)),
},
]
: undefined
@@ -199,6 +202,28 @@ export const ViewsMenu = memo(function ViewsMenu({
</>
)}
</PopoverContent>
<ChipConfirmModal
open={deleteTargetId !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) setDeleteTargetId(null)
}}
srTitle='Delete View'
title='Delete View'
text={[
'Are you sure you want to delete ',
{ text: deleteTarget?.name ?? 'this view', bold: true },
'? ',
{ text: 'This action cannot be undone.', error: true },
]}
confirm={{
label: 'Delete',
onClick: () => {
if (!deleteTargetId) return
onDelete(deleteTargetId)
setDeleteTargetId(null)
},
}}
/>
</Popover>
)
})