Skip to content

Commit 85902eb

Browse files
j15zclaude
andauthored
feat(tables): improve view and filter controls (#6725)
* feat(tables): improve view and filter controls * fix(tables): keep menu actions open * fix(tables): guard view autosave against echo remounts and stale responses Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tables): keep and/or filter toggles, autosave only real edits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tables): compute view-row action spacer and cover the default pin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tables): apply filter text on enter or blur instead of a debounce Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tables): apply filter edits from user events * fix(tables): reject stale default promotions * fix(tables): preserve filters during rule transitions * fix(tables): isolate flagged view interactions * fix(tables): preserve OR boundaries when rules drop * fix(tables): retain deferred filter conditions * fix(tables): keep hidden view actions click-through * fix(tables): keep disabled view pins from selecting the row A disabled Button is pointer-events-none, so clicks on the default or read-only pin fell through the overlay to the row and selected the view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 38630ff commit 85902eb

13 files changed

Lines changed: 1032 additions & 160 deletions

File tree

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,60 @@ describe('SortDropdown', () => {
6767
expect(item?.querySelector('[data-testid="column-icon"]')).not.toBeNull()
6868
expect(item?.querySelectorAll('svg')).toHaveLength(2)
6969
})
70+
71+
it('keeps the popup open while changing or clearing the sort', () => {
72+
const onOpenChange = vi.fn()
73+
const onSort = vi.fn()
74+
const onClear = vi.fn()
75+
act(() => {
76+
root.render(
77+
<SortDropdown
78+
open
79+
onOpenChange={onOpenChange}
80+
config={{
81+
options: [{ id: 'name', label: 'Name', icon: ColumnIcon }],
82+
active: { column: 'name', direction: 'asc' },
83+
onSort,
84+
onClear,
85+
keepOpenOnSelect: true,
86+
}}
87+
/>
88+
)
89+
})
90+
91+
const items = document.body.querySelectorAll<HTMLElement>('[role="menuitem"]')
92+
expect(items).toHaveLength(2)
93+
94+
act(() => items[1]?.click())
95+
expect(onSort).toHaveBeenCalledWith('name', 'desc')
96+
97+
act(() => items[0]?.click())
98+
expect(onClear).toHaveBeenCalledOnce()
99+
expect(onOpenChange).not.toHaveBeenCalledWith(false)
100+
expect(document.body.querySelectorAll('[role="menuitem"]')).toHaveLength(2)
101+
})
102+
103+
it('keeps the legacy close-on-select behavior by default', () => {
104+
const onOpenChange = vi.fn()
105+
const onSort = vi.fn()
106+
act(() => {
107+
root.render(
108+
<SortDropdown
109+
open
110+
onOpenChange={onOpenChange}
111+
config={{
112+
options: [{ id: 'name', label: 'Name', icon: ColumnIcon }],
113+
active: null,
114+
onSort,
115+
}}
116+
/>
117+
)
118+
})
119+
120+
const item = document.body.querySelector<HTMLElement>('[role="menuitem"]')
121+
act(() => item?.click())
122+
123+
expect(onSort).toHaveBeenCalledWith('name', 'desc')
124+
expect(onOpenChange).toHaveBeenCalledWith(false)
125+
})
70126
})

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface SortConfig {
4848
active: { column: string; direction: SortDirection } | null
4949
onSort: (column: string, direction: SortDirection) => void
5050
onClear?: () => void
51+
keepOpenOnSelect?: boolean
5152
}
5253

5354
export interface FilterTag {
@@ -283,7 +284,7 @@ export const SortDropdown = memo(function SortDropdown({
283284
open,
284285
onOpenChange,
285286
}: SortDropdownProps) {
286-
const { options, active, onSort, onClear } = config
287+
const { options, active, onSort, onClear, keepOpenOnSelect = false } = config
287288

288289
return (
289290
<DropdownMenu modal={false} open={open} onOpenChange={onOpenChange}>
@@ -299,7 +300,12 @@ export const SortDropdown = memo(function SortDropdown({
299300
>
300301
{active && onClear && (
301302
<>
302-
<DropdownMenuItem onSelect={onClear}>
303+
<DropdownMenuItem
304+
onSelect={(event) => {
305+
if (keepOpenOnSelect) event.preventDefault()
306+
onClear()
307+
}}
308+
>
303309
<X />
304310
Clear sort
305311
</DropdownMenuItem>
@@ -314,7 +320,8 @@ export const SortDropdown = memo(function SortDropdown({
314320
return (
315321
<DropdownMenuItem
316322
key={option.id}
317-
onSelect={() => {
323+
onSelect={(event) => {
324+
if (keepOpenOnSelect) event.preventDefault()
318325
if (isActive) {
319326
onSort(option.id, active.direction === 'asc' ? 'desc' : 'asc')
320327
} else {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act, useState } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { ColumnsMenu } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu'
8+
9+
let container: HTMLDivElement
10+
let root: Root
11+
12+
function ColumnsMenuHarness({ onChange }: { onChange: (hiddenColumns: string[]) => void }) {
13+
const [hiddenColumns, setHiddenColumns] = useState<string[]>([])
14+
15+
return (
16+
<ColumnsMenu
17+
columns={[
18+
{ id: 'col-name', name: 'Name', type: 'string' },
19+
{ id: 'col-email', name: 'Email', type: 'string' },
20+
{ id: 'col-company', name: 'Company', type: 'string' },
21+
]}
22+
workflowGroups={[]}
23+
hiddenColumns={hiddenColumns}
24+
onChange={(nextHiddenColumns) => {
25+
setHiddenColumns(nextHiddenColumns)
26+
onChange(nextHiddenColumns)
27+
}}
28+
/>
29+
)
30+
}
31+
32+
beforeEach(() => {
33+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
34+
container = document.createElement('div')
35+
document.body.appendChild(container)
36+
root = createRoot(container)
37+
})
38+
39+
afterEach(() => {
40+
act(() => root.unmount())
41+
container.remove()
42+
})
43+
44+
describe('ColumnsMenu', () => {
45+
it('uses the app menu styling and stays open across column changes', () => {
46+
const onChange = vi.fn()
47+
act(() => {
48+
root.render(<ColumnsMenuHarness onChange={onChange} />)
49+
})
50+
act(() => {
51+
container
52+
.querySelector<HTMLButtonElement>('button')
53+
?.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
54+
})
55+
56+
const items = document.body.querySelectorAll<HTMLElement>('[role="menuitem"]')
57+
expect(items).toHaveLength(3)
58+
expect(items[0]).toHaveClass('text-small')
59+
expect(items[0]?.querySelector('svg')).toHaveClass('size-[14px]')
60+
61+
act(() => items[0]?.click())
62+
expect(onChange).toHaveBeenCalledWith(['col-name'])
63+
64+
const remainingItems = document.body.querySelectorAll<HTMLElement>('[role="menuitem"]')
65+
expect(remainingItems).toHaveLength(3)
66+
act(() => remainingItems[1]?.click())
67+
expect(onChange).toHaveBeenLastCalledWith(['col-name', 'col-email'])
68+
})
69+
})

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

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import { memo, useMemo, useState } from 'react'
44
import {
55
Chip,
66
cn,
7-
POPOVER_ANIMATION_CLASSES,
8-
Popover,
9-
PopoverContent,
10-
PopoverItem,
11-
PopoverSection,
12-
PopoverTrigger,
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuTrigger,
1311
} from '@sim/emcn'
1412
import { Columns3, Eye, EyeOff } from '@sim/emcn/icons'
1513
import type { ColumnDefinition, WorkflowGroup } from '@/lib/table'
@@ -78,30 +76,18 @@ export const ColumnsMenu = memo(function ColumnsMenu({
7876
const hiddenCount = hiddenColumns.length
7977

8078
return (
81-
<Popover size='md' open={open} onOpenChange={setOpen}>
82-
<PopoverTrigger asChild>
79+
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
80+
<DropdownMenuTrigger asChild>
8381
{/* `active` alone signals that something is hidden — the label stays fixed
8482
so the bar doesn't reflow as columns are toggled. */}
8583
<Chip active={hiddenCount > 0} leftIcon={Columns3}>
8684
Columns
8785
</Chip>
88-
</PopoverTrigger>
89-
<PopoverContent
90-
side='bottom'
91-
align='start'
92-
sideOffset={6}
93-
minWidth={240}
94-
maxWidth={320}
95-
maxHeight={420}
96-
border
97-
className={cn(
98-
POPOVER_ANIMATION_CLASSES,
99-
'bg-[var(--bg)] p-1.5 text-[var(--text-body)] shadow-sm'
100-
)}
86+
</DropdownMenuTrigger>
87+
<DropdownMenuContent
88+
align='end'
89+
className='max-h-[var(--radix-dropdown-menu-content-available-height,400px)]'
10190
>
102-
<PopoverSection className='px-1.5 py-0.5 text-[var(--text-muted)] text-xs'>
103-
Columns
104-
</PopoverSection>
10591
<div className='flex flex-col gap-0.5'>
10692
{plain.map((col) => {
10793
const id = getColumnId(col)
@@ -144,8 +130,8 @@ export const ColumnsMenu = memo(function ColumnsMenu({
144130
)
145131
})}
146132
</div>
147-
</PopoverContent>
148-
</Popover>
133+
</DropdownMenuContent>
134+
</DropdownMenu>
149135
)
150136
})
151137

@@ -164,14 +150,17 @@ function ColumnToggleRow({ label, visible, partial, indented, onToggle }: Column
164150
const showing = visible || partial
165151
const Icon = showing ? Eye : EyeOff
166152
return (
167-
<PopoverItem
168-
onClick={() => onToggle(!visible)}
169-
className={cn('h-7 items-center gap-1.5 px-1.5 py-0 text-xs', indented && 'pl-5')}
153+
<DropdownMenuItem
154+
onSelect={(event) => {
155+
event.preventDefault()
156+
onToggle(!visible)
157+
}}
158+
className={cn(indented && 'pl-7')}
170159
>
171160
<span className='flex size-[14px] shrink-0 items-center justify-center'>
172161
<Icon
173162
className={cn(
174-
'size-3',
163+
'size-[14px]',
175164
showing ? 'text-[var(--text-icon)]' : 'text-[var(--text-muted)]',
176165
partial && 'opacity-60'
177166
)}
@@ -182,6 +171,6 @@ function ColumnToggleRow({ label, visible, partial, indented, onToggle }: Column
182171
>
183172
{label}
184173
</span>
185-
</PopoverItem>
174+
</DropdownMenuItem>
186175
)
187176
}

0 commit comments

Comments
 (0)