From 130db81579112d070eb184f4688ba64261a9a640 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:03:05 -0700 Subject: [PATCH] improvement(tables): disable the default view's delete action with a tooltip --- .../components/views-menu/views-menu.test.tsx | 18 ++++- .../components/views-menu/views-menu.tsx | 79 ++++++++++++------- 2 files changed, 68 insertions(+), 29 deletions(-) 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 e3eb2598fd6..23a5c633d0e 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 @@ -70,6 +70,7 @@ describe('ViewsMenu', () => { document.body.appendChild(container) const root = createRoot(container) const onSetDefault = vi.fn() + const onDelete = vi.fn() act(() => { root.render( @@ -79,7 +80,7 @@ describe('ViewsMenu', () => { onSelect={vi.fn()} onRename={vi.fn()} onSetDefault={onSetDefault} - onDelete={vi.fn()} + onDelete={onDelete} onNewView={vi.fn()} canEdit /> @@ -87,7 +88,20 @@ describe('ViewsMenu', () => { }) act(() => container.querySelector('button[aria-label="Views"]')?.click()) - expect(document.body.querySelectorAll('button[aria-label="Delete"]')).toHaveLength(1) + // The default view's Delete stays hoverable (aria-disabled, no native + // title) so its tooltip can explain why it is inert. + const deleteButtons = [ + ...document.body.querySelectorAll('button[aria-label="Delete"]'), + ] + expect(deleteButtons).toHaveLength(2) + const defaultDelete = deleteButtons.find( + (button) => button.getAttribute('aria-disabled') === 'true' + ) + expect(defaultDelete).not.toBeUndefined() + expect(defaultDelete?.title).toBe('') + act(() => defaultDelete?.click()) + expect(onDelete).not.toHaveBeenCalled() + const defaultPin = document.body.querySelector( 'button[aria-label="Current default view"]' ) 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 0d545611873..04cb945c8fb 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 @@ -13,6 +13,7 @@ import { PopoverContent, PopoverItem, PopoverSection, + Tooltip, } from '@sim/emcn' import { Check, Pencil, Pin, Plus, Trash } from '@sim/emcn/icons' import type { TableViewWire } from '@/lib/api/contracts/tables' @@ -169,15 +170,14 @@ export const ViewsMenu = memo(function ViewsMenu({ label: 'Rename', onClick: () => runAndClose(() => onRename(view.id)), }, - ...(!view.isDefault - ? [ - { - icon: Trash, - label: 'Delete', - onClick: () => runAndClose(() => onDelete(view.id)), - }, - ] - : []), + { + icon: Trash, + label: 'Delete', + disabledReason: view.isDefault + ? 'Default view cannot be deleted' + : undefined, + onClick: () => runAndClose(() => onDelete(view.id)), + }, ] : undefined } @@ -207,6 +207,8 @@ interface ViewRowAction { icon: React.ElementType label: string onClick: () => void + /** Renders the action inert and dimmed, with this text in its hover tooltip. */ + disabledReason?: string } interface ViewRowDefaultState { @@ -252,24 +254,47 @@ function ViewRow({ label, isActive, onSelect, defaultState, actions }: ViewRowPr {actionCount > 0 && (
- {actions?.map((action) => ( - - ))} + {actions?.map((action) => { + // Disabled via aria-disabled, not the `disabled` attribute: the button + // must keep receiving hover and focus events so the tooltip can explain + // why it is inert, and Button's disabled:opacity-70 would otherwise + // leak it through the hidden (opacity-0) resting state. + const button = ( + + ) + return action.disabledReason ? ( + + {button} + +

{action.disabledReason}

+
+
+ ) : ( + button + ) + })} {defaultState && (