Skip to content

Commit 913af87

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
improvement(ui): share dashed add-row buttons
1 parent d5df6f6 commit 913af87

5 files changed

Lines changed: 84 additions & 24 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
'use client'
22

33
import { useCallback, useMemo } from 'react'
4-
import { Button } from '@sim/emcn'
5-
import { Plus } from '@sim/emcn/icons'
4+
import { AddRowButton } from '@sim/emcn'
65
import { useTableColumns } from '@/lib/table/hooks'
76
import type { FilterRule } from '@/lib/table/query-builder/constants'
87
import { useFilterBuilder } from '@/lib/table/query-builder/use-query-builder'
@@ -83,16 +82,7 @@ export function FilterBuilder({
8382

8483
if (rules.length === 0) {
8584
if (isReadOnly) return null
86-
return (
87-
<Button
88-
variant='ghost'
89-
onClick={addRule}
90-
className='h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small'
91-
>
92-
<Plus className='size-[14px]' />
93-
Add filter condition
94-
</Button>
95-
)
85+
return <AddRowButton onClick={addRule}>Add filter condition</AddRowButton>
9686
}
9787

9888
return (

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
'use client'
22

33
import { useCallback, useMemo } from 'react'
4-
import { Button, type ComboboxOption } from '@sim/emcn'
5-
import { Plus } from '@sim/emcn/icons'
4+
import { AddRowButton, type ComboboxOption } from '@sim/emcn'
65
import { generateId } from '@sim/utils/id'
76
import { useTableColumns } from '@/lib/table/hooks'
87
import { SORT_DIRECTION_OPTIONS, type SortRule } from '@/lib/table/query-builder/constants'
@@ -86,16 +85,7 @@ export function SortBuilder({
8685

8786
if (rules.length === 0) {
8887
if (isReadOnly) return null
89-
return (
90-
<Button
91-
variant='ghost'
92-
onClick={addRule}
93-
className='h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small'
94-
>
95-
<Plus className='size-[14px]' />
96-
Add sort
97-
</Button>
98-
)
88+
return <AddRowButton onClick={addRule}>Add sort</AddRowButton>
9989
}
10090

10191
return (
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/** @vitest-environment jsdom */
2+
import { act, createRef } from 'react'
3+
import { AddRowButton } from '@sim/emcn'
4+
import { createRoot, type Root } from 'react-dom/client'
5+
import { afterEach, describe, expect, it, vi } from 'vitest'
6+
7+
let root: Root | null = null
8+
let container: HTMLDivElement | null = null
9+
10+
afterEach(() => {
11+
if (root) act(() => root?.unmount())
12+
container?.remove()
13+
root = null
14+
container = null
15+
})
16+
17+
describe('AddRowButton', () => {
18+
it('forwards its ref and action, honors disabled, and preserves native form submission', () => {
19+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
20+
container = document.createElement('div')
21+
document.body.appendChild(container)
22+
root = createRoot(container)
23+
const ref = createRef<HTMLButtonElement>()
24+
const onClick = vi.fn()
25+
const onSubmit = vi.fn((event) => event.preventDefault())
26+
const render = (disabled: boolean) => (
27+
<form onSubmit={onSubmit}>
28+
<AddRowButton ref={ref} onClick={onClick} disabled={disabled}>
29+
Add filter condition
30+
</AddRowButton>
31+
</form>
32+
)
33+
act(() => root?.render(render(false)))
34+
const button = ref.current!
35+
expect(button).toBe(container.querySelector('button'))
36+
expect(button.textContent).toBe('Add filter condition')
37+
expect(button.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true')
38+
act(() => button.click())
39+
expect(onClick).toHaveBeenCalledOnce()
40+
expect(onSubmit).toHaveBeenCalledOnce()
41+
42+
act(() => root?.render(render(true)))
43+
expect(button.disabled).toBe(true)
44+
act(() => button.click())
45+
expect(onClick).toHaveBeenCalledOnce()
46+
})
47+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { type ComponentPropsWithoutRef, forwardRef } from 'react'
2+
import { Plus } from '../../icons/plus'
3+
import { cn } from '../../lib/cn'
4+
import { Button } from '../button/button'
5+
6+
export type AddRowButtonProps = ComponentPropsWithoutRef<'button'>
7+
8+
/**
9+
* Full-width dashed action for adding the first row to an empty editor list.
10+
* Keeps the existing 28px filter/sort treatment; list state stays with the caller.
11+
* Forwards native button props and refs, preserving Button's native form behavior.
12+
*
13+
* @example <AddRowButton onClick={addRule}>Add filter condition</AddRowButton>
14+
*/
15+
export const AddRowButton = forwardRef<HTMLButtonElement, AddRowButtonProps>(
16+
({ children, className, ...props }, ref) => (
17+
<Button
18+
{...props}
19+
ref={ref}
20+
variant='ghost'
21+
className={cn(
22+
'h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small',
23+
className
24+
)}
25+
>
26+
<Plus className='size-[14px]' />
27+
{children}
28+
</Button>
29+
)
30+
)
31+
32+
AddRowButton.displayName = 'AddRowButton'

‎packages/emcn/src/components/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { AddRowButton, type AddRowButtonProps } from './add-row-button/add-row-button'
12
export { Avatar, AvatarFallback, AvatarImage } from './avatar/avatar'
23
export { Badge, type BadgeProps } from './badge/badge'
34
export { Banner } from './banner/banner'

0 commit comments

Comments
 (0)