From 913af877c453089d09ad5ee5c5e8dc7ab636b9a3 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Sat, 19 Sep 2026 13:46:37 -0700 Subject: [PATCH] improvement(ui): share dashed add-row buttons --- .../filter-builder/filter-builder.tsx | 14 +----- .../components/sort-builder/sort-builder.tsx | 14 +----- .../add-row-button/add-row-button.test.tsx | 47 +++++++++++++++++++ .../add-row-button/add-row-button.tsx | 32 +++++++++++++ packages/emcn/src/components/index.ts | 1 + 5 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 packages/emcn/src/components/add-row-button/add-row-button.test.tsx create mode 100644 packages/emcn/src/components/add-row-button/add-row-button.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx index 4df315eb4c4..a716ed258e3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx @@ -1,8 +1,7 @@ 'use client' import { useCallback, useMemo } from 'react' -import { Button } from '@sim/emcn' -import { Plus } from '@sim/emcn/icons' +import { AddRowButton } from '@sim/emcn' import { useTableColumns } from '@/lib/table/hooks' import type { FilterRule } from '@/lib/table/query-builder/constants' import { useFilterBuilder } from '@/lib/table/query-builder/use-query-builder' @@ -83,16 +82,7 @@ export function FilterBuilder({ if (rules.length === 0) { if (isReadOnly) return null - return ( - - ) + return Add filter condition } return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx index c9963668a00..d4092384e9d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx @@ -1,8 +1,7 @@ 'use client' import { useCallback, useMemo } from 'react' -import { Button, type ComboboxOption } from '@sim/emcn' -import { Plus } from '@sim/emcn/icons' +import { AddRowButton, type ComboboxOption } from '@sim/emcn' import { generateId } from '@sim/utils/id' import { useTableColumns } from '@/lib/table/hooks' import { SORT_DIRECTION_OPTIONS, type SortRule } from '@/lib/table/query-builder/constants' @@ -86,16 +85,7 @@ export function SortBuilder({ if (rules.length === 0) { if (isReadOnly) return null - return ( - - ) + return Add sort } return ( diff --git a/packages/emcn/src/components/add-row-button/add-row-button.test.tsx b/packages/emcn/src/components/add-row-button/add-row-button.test.tsx new file mode 100644 index 00000000000..dfd6cb20e1f --- /dev/null +++ b/packages/emcn/src/components/add-row-button/add-row-button.test.tsx @@ -0,0 +1,47 @@ +/** @vitest-environment jsdom */ +import { act, createRef } from 'react' +import { AddRowButton } from '@sim/emcn' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' + +let root: Root | null = null +let container: HTMLDivElement | null = null + +afterEach(() => { + if (root) act(() => root?.unmount()) + container?.remove() + root = null + container = null +}) + +describe('AddRowButton', () => { + it('forwards its ref and action, honors disabled, and preserves native form submission', () => { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + const ref = createRef() + const onClick = vi.fn() + const onSubmit = vi.fn((event) => event.preventDefault()) + const render = (disabled: boolean) => ( +
+ + Add filter condition + +
+ ) + act(() => root?.render(render(false))) + const button = ref.current! + expect(button).toBe(container.querySelector('button')) + expect(button.textContent).toBe('Add filter condition') + expect(button.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true') + act(() => button.click()) + expect(onClick).toHaveBeenCalledOnce() + expect(onSubmit).toHaveBeenCalledOnce() + + act(() => root?.render(render(true))) + expect(button.disabled).toBe(true) + act(() => button.click()) + expect(onClick).toHaveBeenCalledOnce() + }) +}) diff --git a/packages/emcn/src/components/add-row-button/add-row-button.tsx b/packages/emcn/src/components/add-row-button/add-row-button.tsx new file mode 100644 index 00000000000..b34dedaa536 --- /dev/null +++ b/packages/emcn/src/components/add-row-button/add-row-button.tsx @@ -0,0 +1,32 @@ +import { type ComponentPropsWithoutRef, forwardRef } from 'react' +import { Plus } from '../../icons/plus' +import { cn } from '../../lib/cn' +import { Button } from '../button/button' + +export type AddRowButtonProps = ComponentPropsWithoutRef<'button'> + +/** + * Full-width dashed action for adding the first row to an empty editor list. + * Keeps the existing 28px filter/sort treatment; list state stays with the caller. + * Forwards native button props and refs, preserving Button's native form behavior. + * + * @example Add filter condition + */ +export const AddRowButton = forwardRef( + ({ children, className, ...props }, ref) => ( + + ) +) + +AddRowButton.displayName = 'AddRowButton' diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts index c7c2204968a..0848262daa5 100644 --- a/packages/emcn/src/components/index.ts +++ b/packages/emcn/src/components/index.ts @@ -1,3 +1,4 @@ +export { AddRowButton, type AddRowButtonProps } from './add-row-button/add-row-button' export { Avatar, AvatarFallback, AvatarImage } from './avatar/avatar' export { Badge, type BadgeProps } from './badge/badge' export { Banner } from './banner/banner'