From 30e7dcddc89234f355e8eb5579a0e824198813a9 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:07:59 -0700 Subject: [PATCH] feat(tables): confirm view deletion --- .../components/views-menu/views-menu.test.tsx | 63 +++++++++++++++++++ .../components/views-menu/views-menu.tsx | 27 +++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.test.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.test.tsx index 23a5c633d0e..3f57c4f3ee0 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.test.tsx @@ -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( + + ) + }) + act(() => container.querySelector('button[aria-label="Views"]')?.click()) + + const getDeleteButton = () => + [...document.body.querySelectorAll('button[aria-label="Delete"]')].find( + (button) => button.getAttribute('aria-disabled') !== 'true' + ) + const getConfirmationDialog = () => + [...document.body.querySelectorAll('[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('button') ?? []), + ].find((button) => button.textContent === 'Cancel') + act(() => cancelButton?.click()) + + expect(getConfirmationDialog()).toBeUndefined() + expect(onDelete).not.toHaveBeenCalled() + + act(() => container.querySelector('button[aria-label="Views"]')?.click()) + act(() => getDeleteButton()?.click()) + + const dialog = getConfirmationDialog() + const confirmButton = [...(dialog?.querySelectorAll('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') diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx index 04cb945c8fb..cffa4a09d1f 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx @@ -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(null) const closeTimeoutRef = useRef | 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({ )} + { + 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) + }, + }} + /> ) })