mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-01 14:59:19 +08:00
improvement(tables): disable the default view's delete action with a tooltip (#6897)
This commit is contained in:
committed by
GitHub
parent
97c1688c49
commit
43850e3567
+16
-2
@@ -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<HTMLButtonElement>('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<HTMLButtonElement>('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<HTMLButtonElement>(
|
||||
'button[aria-label="Current default view"]'
|
||||
)
|
||||
|
||||
+52
-27
@@ -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
|
||||
</PopoverItem>
|
||||
{actionCount > 0 && (
|
||||
<div className='pointer-events-none absolute right-1.5 flex items-center gap-0.5'>
|
||||
{actions?.map((action) => (
|
||||
<Button
|
||||
key={action.label}
|
||||
type='button'
|
||||
variant='quiet'
|
||||
size='icon'
|
||||
aria-label={action.label}
|
||||
title={action.label}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
action.onClick()
|
||||
}}
|
||||
className='pointer-events-none opacity-0 transition-[background-color,color,opacity] group-focus-within/view:pointer-events-auto group-focus-within/view:opacity-100 group-hover/view:pointer-events-auto group-hover/view:opacity-100'
|
||||
>
|
||||
<action.icon className='size-3' />
|
||||
</Button>
|
||||
))}
|
||||
{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 = (
|
||||
<Button
|
||||
key={action.label}
|
||||
type='button'
|
||||
variant='quiet'
|
||||
size='icon'
|
||||
aria-label={action.label}
|
||||
title={action.disabledReason ? undefined : action.label}
|
||||
aria-disabled={action.disabledReason ? true : undefined}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (action.disabledReason) return
|
||||
action.onClick()
|
||||
}}
|
||||
className={cn(
|
||||
'pointer-events-none opacity-0 transition-[background-color,color,opacity] group-focus-within/view:pointer-events-auto group-hover/view:pointer-events-auto',
|
||||
action.disabledReason
|
||||
? 'cursor-default hover-hover:bg-transparent group-focus-within/view:opacity-40 group-hover/view:opacity-40'
|
||||
: 'group-focus-within/view:opacity-100 group-hover/view:opacity-100'
|
||||
)}
|
||||
>
|
||||
<action.icon className='size-3' />
|
||||
</Button>
|
||||
)
|
||||
return action.disabledReason ? (
|
||||
<Tooltip.Root key={action.label}>
|
||||
<Tooltip.Trigger asChild>{button}</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{action.disabledReason}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
) : (
|
||||
button
|
||||
)
|
||||
})}
|
||||
{defaultState && (
|
||||
<Button
|
||||
type='button'
|
||||
|
||||
Reference in New Issue
Block a user