Skip to content

Commit 472532e

Browse files
authored
feat(tables): confirm view deletion (#7041)
1 parent 6d313a4 commit 472532e

2 files changed

Lines changed: 89 additions & 1 deletion

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.test.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,69 @@ describe('ViewsMenu', () => {
125125
container.remove()
126126
})
127127

128+
it('confirms before deleting a saved view', () => {
129+
const container = document.createElement('div')
130+
document.body.appendChild(container)
131+
const root = createRoot(container)
132+
const onDelete = vi.fn()
133+
134+
act(() => {
135+
root.render(
136+
<ViewsMenu
137+
views={[PRIMARY_VIEW, SECOND_VIEW]}
138+
activeViewId={PRIMARY_VIEW.id}
139+
onSelect={vi.fn()}
140+
onRename={vi.fn()}
141+
onSetDefault={vi.fn()}
142+
onDelete={onDelete}
143+
onNewView={vi.fn()}
144+
canEdit
145+
/>
146+
)
147+
})
148+
act(() => container.querySelector<HTMLButtonElement>('button[aria-label="Views"]')?.click())
149+
150+
const getDeleteButton = () =>
151+
[...document.body.querySelectorAll<HTMLButtonElement>('button[aria-label="Delete"]')].find(
152+
(button) => button.getAttribute('aria-disabled') !== 'true'
153+
)
154+
const getConfirmationDialog = () =>
155+
[...document.body.querySelectorAll<HTMLElement>('[role="dialog"]')].find((dialog) =>
156+
dialog.textContent?.includes('This action cannot be undone.')
157+
)
158+
159+
act(() => getDeleteButton()?.click())
160+
161+
expect(onDelete).not.toHaveBeenCalled()
162+
const firstDialog = getConfirmationDialog()
163+
expect(firstDialog).toHaveTextContent('Delete View')
164+
expect(firstDialog).toHaveTextContent('Second view')
165+
expect(firstDialog).toHaveTextContent('This action cannot be undone.')
166+
167+
const cancelButton = [
168+
...(firstDialog?.querySelectorAll<HTMLButtonElement>('button') ?? []),
169+
].find((button) => button.textContent === 'Cancel')
170+
act(() => cancelButton?.click())
171+
172+
expect(getConfirmationDialog()).toBeUndefined()
173+
expect(onDelete).not.toHaveBeenCalled()
174+
175+
act(() => container.querySelector<HTMLButtonElement>('button[aria-label="Views"]')?.click())
176+
act(() => getDeleteButton()?.click())
177+
178+
const dialog = getConfirmationDialog()
179+
const confirmButton = [...(dialog?.querySelectorAll<HTMLButtonElement>('button') ?? [])].find(
180+
(button) => button.textContent === 'Delete'
181+
)
182+
act(() => confirmButton?.click())
183+
184+
expect(onDelete).toHaveBeenCalledOnce()
185+
expect(onDelete).toHaveBeenCalledWith(SECOND_VIEW.id)
186+
187+
act(() => root.unmount())
188+
container.remove()
189+
})
190+
128191
it('keeps the menu open when keyboard focus moves from the trigger to the default pin', () => {
129192
vi.useFakeTimers()
130193
const container = document.createElement('div')

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { memo, useEffect, useRef, useState } from 'react'
44
import {
55
Button,
66
ChipChevronDown,
7+
ChipConfirmModal,
78
chipContentLabelClass,
89
chipVariants,
910
cn,
@@ -62,9 +63,11 @@ export const ViewsMenu = memo(function ViewsMenu({
6263
canEdit,
6364
}: ViewsMenuProps) {
6465
const [open, setOpen] = useState(false)
66+
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
6567
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
6668

6769
const { activeView, defaultView } = resolveTableViewSelection(views, activeViewId)
70+
const deleteTarget = views.find((view) => view.id === deleteTargetId)
6871
const hasDefaultView = defaultView !== null
6972
const label = activeView?.name ?? ALL_ROWS_VIEW_LABEL
7073

@@ -176,7 +179,7 @@ export const ViewsMenu = memo(function ViewsMenu({
176179
disabledReason: view.isDefault
177180
? 'Default view cannot be deleted'
178181
: undefined,
179-
onClick: () => runAndClose(() => onDelete(view.id)),
182+
onClick: () => runAndClose(() => setDeleteTargetId(view.id)),
180183
},
181184
]
182185
: undefined
@@ -199,6 +202,28 @@ export const ViewsMenu = memo(function ViewsMenu({
199202
</>
200203
)}
201204
</PopoverContent>
205+
<ChipConfirmModal
206+
open={deleteTargetId !== null}
207+
onOpenChange={(nextOpen) => {
208+
if (!nextOpen) setDeleteTargetId(null)
209+
}}
210+
srTitle='Delete View'
211+
title='Delete View'
212+
text={[
213+
'Are you sure you want to delete ',
214+
{ text: deleteTarget?.name ?? 'this view', bold: true },
215+
'? ',
216+
{ text: 'This action cannot be undone.', error: true },
217+
]}
218+
confirm={{
219+
label: 'Delete',
220+
onClick: () => {
221+
if (!deleteTargetId) return
222+
onDelete(deleteTargetId)
223+
setDeleteTargetId(null)
224+
},
225+
}}
226+
/>
202227
</Popover>
203228
)
204229
})

0 commit comments

Comments
 (0)