From b8c83e89ec331724b9038dda31ec95242d0583bd Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 22 Sep 2026 13:42:29 -0700 Subject: [PATCH 1/3] refactor(ui): share static API and MCP parameter cards --- .../workflow-mcp-servers.tsx | 48 ++++---- .../components/table-grid/data-row.tsx | 6 +- .../general/components/api-info-modal.tsx | 38 +++---- .../deploy-modal/components/mcp/mcp.tsx | 54 ++++----- .../condition-input/condition-input.tsx | 8 +- .../document-tag-entry/document-tag-entry.tsx | 9 +- .../components/eval-input/eval-input.tsx | 4 +- .../field-mode-toggle/field-mode-toggle.tsx | 37 +++++++ .../components/filter-rule-row.tsx | 4 +- .../knowledge-tag-filters.tsx | 4 +- .../selector-combobox/selector-combobox.tsx | 3 +- .../sort-builder/components/sort-rule-row.tsx | 4 +- .../components/starter/input-format.tsx | 51 +++------ .../components/tools/usage-control.tsx | 33 ++---- .../variables-input/variables-input.tsx | 49 +++------ .../editor/components/sub-block/sub-block.tsx | 52 ++------- .../components/variables/variables.tsx | 2 +- .../collapsible-card.test.tsx | 18 ++- .../collapsible-card/collapsible-card.tsx | 104 ++++++++---------- .../src/components/field-card/field-card.tsx | 74 +++++++++++++ packages/emcn/src/components/index.ts | 1 + 21 files changed, 308 insertions(+), 295 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/field-mode-toggle/field-mode-toggle.tsx create mode 100644 packages/emcn/src/components/field-card/field-card.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx index c0bbd87f022..62348517213 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx @@ -18,6 +18,7 @@ import { ChipSelect, Code, type ComboboxOption, + FieldCard, Label, useCopyToClipboard, } from '@sim/emcn' @@ -713,36 +714,29 @@ function ServerDetailView({ return hasParams ? (
{Object.entries(properties).map(([name, prop]) => ( -
+ {prop.type || 'any'} + + } > -
-
- - {name} - - - {prop.type || 'any'} - -
-
-
-
- - - setEditingParameterDescriptions((prev) => ({ - ...prev, - [name]: e.target.value, - })) - } - placeholder={`Enter description for ${name}`} - /> -
+
+ + + setEditingParameterDescriptions((prev) => ({ + ...prev, + [name]: e.target.value, + })) + } + placeholder={`Enter description for ${name}`} + />
-
+ ))}
) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx index 0c603a6e1dc..5321fbf0140 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx @@ -287,11 +287,11 @@ export const DataRow = React.memo(function DataRow({ {hasWorkflowColumns && ( @@ -270,7 +265,7 @@ export function DocumentTagEntry({ variant='ghost-destructive' onClick={() => removeTag(tag.id)} disabled={isReadOnly} - className='h-auto p-0' + size='icon' > Delete Tag diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx index 652f79b6e3a..a8c87277a31 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx @@ -147,7 +147,7 @@ export function EvalInput({ variant='ghost' onClick={addMetric} disabled={isPreview || disabled} - className='h-auto p-0' + size='icon' > Add Metric @@ -162,7 +162,7 @@ export function EvalInput({ variant='ghost-destructive' onClick={() => removeMetric(metric.id)} disabled={isPreview || disabled || metrics.length === 1} - className='h-auto p-0' + size='icon' > Delete Metric diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/field-mode-toggle/field-mode-toggle.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/field-mode-toggle/field-mode-toggle.tsx new file mode 100644 index 00000000000..e7916f529d2 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/field-mode-toggle/field-mode-toggle.tsx @@ -0,0 +1,37 @@ +import type { ButtonHTMLAttributes } from 'react' +import { cn, Tooltip } from '@sim/emcn' +import { ArrowLeftRight } from '@sim/emcn/icons' + +interface FieldModeToggleProps { + label: string + active: boolean + disabled?: boolean + onClick: ButtonHTMLAttributes['onClick'] +} + +/** Shared field-mode affordance; the caller owns the mode and its stored values. */ +export function FieldModeToggle({ label, active, disabled, onClick }: FieldModeToggleProps) { + return ( + + + + + +

{label}

+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/components/filter-rule-row.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/components/filter-rule-row.tsx index 7e0e4375b3b..33a1fc95fc7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/components/filter-rule-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/components/filter-rule-row.tsx @@ -129,7 +129,7 @@ export function FilterRuleRow({ const renderActions = () => ( <> - @@ -137,7 +137,7 @@ export function FilterRuleRow({ variant='ghost-destructive' onClick={() => onRemove(rule.id)} disabled={isReadOnly} - className='h-auto p-0' + size='icon' > Delete Condition diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx index dc4e6b62c34..0621ae5171b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -257,7 +257,7 @@ export function KnowledgeTagFilters({ addFilter() }} disabled={isReadOnly} - className='h-auto p-0' + size='icon' > Add Filter @@ -269,7 +269,7 @@ export function KnowledgeTagFilters({ removeFilter(filter.id) }} disabled={isReadOnly} - className='h-auto p-0' + size='icon' > Delete Filter diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx index 720a511c18c..dc3a6973a4b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx @@ -263,7 +263,8 @@ export function SelectorCombobox({ @@ -96,7 +96,7 @@ export function SortRuleRow({ variant='ghost-destructive' onClick={() => onRemove(rule.id)} disabled={isReadOnly} - className='h-auto p-0' + size='icon' > Delete Sort diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx index c23c18ba66c..a949d97a61b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx @@ -14,15 +14,15 @@ import { Label, languages, OverflowText, - Tooltip, } from '@sim/emcn' -import { ArrowLeftRight, Plus, Trash } from '@sim/emcn/icons' +import { Plus, Trash } from '@sim/emcn/icons' import Editor from 'react-simple-code-editor' import { createDefaultInputFormatField, isFileFieldType, parseInputFormatFiles, } from '@/lib/workflows/input-format' +import { FieldModeToggle } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/field-mode-toggle/field-mode-toggle' import { FileUpload } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { @@ -166,32 +166,17 @@ export function FieldFormat({ if (!canUseUploader) return null const label = mode === 'upload' ? 'Switch to JSON' : 'Switch to file uploader' return ( - - - - - -

{label}

-
-
+ + setFileFieldModes((prev) => ({ + ...prev, + [field.id]: mode === 'upload' ? 'json' : 'upload', + })) + } + /> ) } @@ -647,12 +632,7 @@ export function FieldFormat({ animated actions={ <> - @@ -660,7 +640,8 @@ export function FieldFormat({ variant='ghost-destructive' onClick={() => removeField(field.id)} disabled={isReadOnly} - className='h-auto p-0 hover-hover:opacity-90' + size='icon' + className='hover-hover:opacity-90' > Delete Field diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control.tsx index e79ab62abcd..684602b559c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control.tsx @@ -1,7 +1,7 @@ -import { Combobox, cn, Label, Tooltip } from '@sim/emcn' -import { ArrowLeftRight } from '@sim/emcn/icons' +import { Combobox, Label } from '@sim/emcn' import type { CanonicalMode } from '@/lib/workflows/subblocks/visibility' import type { StoredTool } from '@/lib/workflows/tool-input/types' +import { FieldModeToggle } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/field-mode-toggle/field-mode-toggle' import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input' import type { SubBlockConfig } from '@/blocks/types' @@ -63,29 +63,12 @@ export function ToolUsageControl({
- - - - - -

{toggleLabel}

-
-
+
{mode === 'advanced' ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx index 85da29e76f4..84b832a7cbb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx @@ -10,11 +10,11 @@ import { Label, OverflowText, Textarea, - Tooltip, } from '@sim/emcn' -import { ArrowLeftRight, FileText, Plus, Trash } from '@sim/emcn/icons' +import { FileText, Plus, Trash } from '@sim/emcn/icons' import { generateId } from '@sim/utils/id' import { useParams } from 'next/navigation' +import { FieldModeToggle } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/field-mode-toggle/field-mode-toggle' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { checkTagTrigger, @@ -416,7 +416,7 @@ export function VariablesInput({ addAssignment() }} disabled={isReadOnly || allVariablesAssigned} - className='h-auto p-0' + size='icon' > Add Variable @@ -428,7 +428,7 @@ export function VariablesInput({ removeAssignment(assignment.id) }} disabled={isReadOnly} - className='h-auto p-0' + size='icon' > Delete Variable @@ -460,36 +460,17 @@ export function VariablesInput({
{assignment.type === 'boolean' && ( - - - - - -

{isManualBoolean ? 'Switch to selector' : 'Switch to manual value'}

-
-
+ + setManualBooleanModes((prev) => ({ + ...prev, + [assignment.id]: !isManualBoolean, + })) + } + /> )}
{assignment.type === 'boolean' && !isManualBoolean ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 4795928d1c6..7b40f64332f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -1,13 +1,6 @@ import { type JSX, type MouseEvent, memo, useCallback, useMemo, useRef, useState } from 'react' import { Button, cn, Input, Label, Tooltip } from '@sim/emcn' -import { - ArrowLeftRight, - ArrowUp, - Check, - Clipboard, - SquareArrowUpRight, - TriangleAlert, -} from '@sim/emcn/icons' +import { ArrowUp, Check, Clipboard, SquareArrowUpRight, TriangleAlert } from '@sim/emcn/icons' import { isEqual } from 'es-toolkit' import { useParams } from 'next/navigation' import type { FilterRule, SortRule } from '@/lib/table/query-builder/constants' @@ -54,6 +47,7 @@ import { WorkflowSelectorInput, WorkspaceFolderSelector, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' +import { FieldModeToggle } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/field-mode-toggle/field-mode-toggle' import { MODAL_REGISTRY } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/modal-registry' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import type { SubBlockConfig } from '@/blocks/types' @@ -351,7 +345,8 @@ const renderLabel = ( e.stopPropagation() wandState.onSearchSubmit() }} - className='size-[20px] shrink-0 p-0' + size='icon' + className='shrink-0' > @@ -377,37 +372,14 @@ const renderLabel = ( )} {showCanonicalToggle && ( - - - - - -

- {canonicalToggle?.mode === 'advanced' - ? 'Switch to selector' - : 'Switch to manual ID'} -

-
-
+ )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx index 61ca1967060..a836389a65b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx @@ -457,7 +457,7 @@ export function Variables({ readOnly = false }: VariablesProps) { e.stopPropagation() handleRemoveVariable(variable.id) }} - className='h-auto p-0' + size='icon' disabled={readOnly} aria-label={`Delete ${variable.name || `variable ${index + 1}`}`} > diff --git a/packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx b/packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx index 2a46d78eaef..a61dba4b2eb 100644 --- a/packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx +++ b/packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx @@ -1,6 +1,6 @@ /** @vitest-environment jsdom */ import { act, type ReactNode, useState } from 'react' -import { CollapsibleCard } from '@sim/emcn' +import { CollapsibleCard, FieldCard } from '@sim/emcn' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -103,3 +103,19 @@ describe('CollapsibleCard', () => { expect(parentClick).not.toHaveBeenCalled() }) }) + +it('keeps static FieldCard content visible without a collapse target', () => { + mount( + string} data-field='query'> + + + ) + const card = container!.querySelector('[data-field="query"]')! + const input = card.querySelector('input')! + expect(card.textContent).toContain('query') + expect(card.textContent).toContain('string') + expect(card.querySelector('[role="button"], button, [aria-expanded]')).toBeNull() + act(() => card.querySelector('[data-overflow-text]')!.click()) + expect(card.querySelector('input')).toBe(input) + expect(input.value).toBe('Search terms') +}) diff --git a/packages/emcn/src/components/collapsible-card/collapsible-card.tsx b/packages/emcn/src/components/collapsible-card/collapsible-card.tsx index a2e9a640c68..9104661a7ea 100644 --- a/packages/emcn/src/components/collapsible-card/collapsible-card.tsx +++ b/packages/emcn/src/components/collapsible-card/collapsible-card.tsx @@ -4,6 +4,7 @@ import type * as React from 'react' import { cn } from '../../lib/cn' import { handleKeyboardActivation } from '../../lib/keyboard' import { Expandable, ExpandableContent } from '../expandable/expandable' +import { FieldCardContent, FieldCardFrame } from '../field-card/field-card' import { OverflowText, overflowTextClipClass } from '../overflow-text/overflow-text' export interface CollapsibleCardProps @@ -28,6 +29,7 @@ export interface CollapsibleCardProps * A collapsible field card: a `--surface-4` header (click / keyboard to toggle) * with a fade-clipped title + optional badge, over a `--surface-2` body. Shared by * the workflow input-mapping rows and the enrichment output-column config. + * Its frame and body are also used by the always-open FieldCard. * * @example * - {children} - - ) + const content = {children} return ( -
-
-
{ - if (event.target !== event.currentTarget) return - handleKeyboardActivation(event, onToggleCollapse) - }} - > - {typeof title === 'string' || typeof title === 'number' ? ( - - ) : ( - - {title} - - )} - {badge} -
- {actions && ( + className={cn(collapsed ? 'overflow-hidden' : 'overflow-visible', className)} + header={ + <>
event.stopPropagation()} + role='button' + tabIndex={0} + aria-expanded={!collapsed} + aria-controls={contentProps?.id} + className={cn( + 'flex min-w-0 flex-1 cursor-pointer items-center gap-2 px-2.5 py-[5px]', + actions && 'pr-2' + )} + onClick={onToggleCollapse} + onKeyDown={(event) => { + if (event.target !== event.currentTarget) return + handleKeyboardActivation(event, onToggleCollapse) + }} > - {actions} + {typeof title === 'string' || typeof title === 'number' ? ( + + ) : ( + + {title} + + )} + {badge}
- )} -
+ {actions && ( +
event.stopPropagation()} + > + {actions} +
+ )} + + } + > {animated ? ( {content} @@ -115,6 +105,6 @@ export function CollapsibleCard({ ) : ( !collapsed && content )} -
+ ) } diff --git a/packages/emcn/src/components/field-card/field-card.tsx b/packages/emcn/src/components/field-card/field-card.tsx new file mode 100644 index 00000000000..c5f05745635 --- /dev/null +++ b/packages/emcn/src/components/field-card/field-card.tsx @@ -0,0 +1,74 @@ +import type * as React from 'react' +import { cn } from '../../lib/cn' +import { OverflowText, overflowTextClipClass } from '../overflow-text/overflow-text' + +export interface FieldCardProps + extends Omit, 'title' | 'children'> { + /** Field name; plain text uses the standard fade and full-value tooltip. */ + title: React.ReactNode + /** Optional content adjacent to the field name, such as a type badge. */ + badge?: React.ReactNode + /** Always-visible field controls or description. */ + children: React.ReactNode +} + +interface FieldCardFrameProps extends React.HTMLAttributes { + header: React.ReactNode +} + +/** Internal frame shared by static and collapsible field cards. */ +export function FieldCardFrame({ header, children, className, ...props }: FieldCardFrameProps) { + return ( +
+
+ {header} +
+ {children} +
+ ) +} + +/** Internal body shared by static and collapsible field cards. */ +export function FieldCardContent({ className, ...props }: React.HTMLAttributes) { + return ( +
+ ) +} + +/** + * An always-open field card with the same frame and body as CollapsibleCard. + * The header is a label, not a button; controls retain their own behavior. + * + * @example + * string}> + * {descriptionField} + * + */ +export function FieldCard({ title, badge, children, className, ...props }: FieldCardProps) { + return ( + + {typeof title === 'string' || typeof title === 'number' ? ( + + ) : ( + + {title} + + )} + {badge} +
+ } + > + {children} + + ) +} diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts index b194122e642..b26ee162548 100644 --- a/packages/emcn/src/components/index.ts +++ b/packages/emcn/src/components/index.ts @@ -150,6 +150,7 @@ export { dropdownMenuRowClass, } from './dropdown-menu/dropdown-menu' export { Expandable, ExpandableContent } from './expandable/expandable' +export { FieldCard, type FieldCardProps } from './field-card/field-card' export { DashedDividerLine, FieldDivider } from './field-divider/field-divider' export { Info } from './info/info' export { From faa970e0575a14c5dbc432a4c4470a5437873ce3 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 22 Sep 2026 13:57:18 -0700 Subject: [PATCH 2/3] fix(ui): preserve specialized tiny action geometry --- .../knowledge-tag-filters/knowledge-tag-filters.tsx | 4 ++-- .../w/[workflowId]/components/variables/variables.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx index 0621ae5171b..dc4e6b62c34 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -257,7 +257,7 @@ export function KnowledgeTagFilters({ addFilter() }} disabled={isReadOnly} - size='icon' + className='h-auto p-0' > Add Filter @@ -269,7 +269,7 @@ export function KnowledgeTagFilters({ removeFilter(filter.id) }} disabled={isReadOnly} - size='icon' + className='h-auto p-0' > Delete Filter diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx index a836389a65b..61ca1967060 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx @@ -457,7 +457,7 @@ export function Variables({ readOnly = false }: VariablesProps) { e.stopPropagation() handleRemoveVariable(variable.id) }} - size='icon' + className='h-auto p-0' disabled={readOnly} aria-label={`Delete ${variable.name || `variable ${index + 1}`}`} > From 99ca82b20476b29c0a4c5c518933a94978954ed7 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos <157128530+BillLeoutsakosvl346@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:13:19 -0700 Subject: [PATCH 3/3] improvement(ui): reuse collapsible cards for MCP tools (#8170) Co-authored-by: Bill Leoutsakos --- .../settings/components/mcp/mcp.tsx | 107 +++++++++--------- .../collapsible-card.test.tsx | 32 ++++++ .../collapsible-card/collapsible-card.tsx | 13 ++- 3 files changed, 96 insertions(+), 56 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx index 488a0ef206c..3a04667501f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx @@ -1,7 +1,16 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { Badge, Button, Chip, ChipConfirmModal, cn, Tooltip, toast } from '@sim/emcn' +import { + Badge, + Chip, + ChipConfirmModal, + CollapsibleCard, + cn, + OverflowText, + Tooltip, + toast, +} from '@sim/emcn' import { ArrowLeft, ChevronDown, Plus } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' @@ -535,59 +544,53 @@ export function MCP() { const requiredParams = tool.inputSchema?.required || [] return ( -
- - + + } + badge={ + <> + {issues.length > 0 && ( + + + + + {getIssueBadgeLabel(issues[0].issue)} + + + + + Update in: {affectedWorkflows.join(', ')} + + + )} + {hasParams && ( + + )} + + } + > {isExpanded && hasParams && ( -
-

+ <> +

Parameters

@@ -631,9 +634,9 @@ export function MCP() { } )}
-
+ )} -
+
) })} diff --git a/packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx b/packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx index a61dba4b2eb..12da6d82427 100644 --- a/packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx +++ b/packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx @@ -62,6 +62,38 @@ describe('CollapsibleCard', () => { } ) + it('prevents disabled header activation without disabling independent actions', () => { + const toggle = vi.fn() + const action = vi.fn() + mount( + + Refresh + + } + > + Content + + ) + const trigger = container!.querySelector('[role="button"]')! + expect(trigger.tabIndex).toBe(-1) + expect(trigger.getAttribute('aria-disabled')).toBe('true') + act(() => { + trigger.click() + trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) + trigger.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })) + container!.querySelector('button')!.click() + }) + expect(toggle).not.toHaveBeenCalled() + expect(action).toHaveBeenCalledTimes(1) + expect(container!.textContent).not.toContain('Content') + }) + it('keeps enabled and disabled actions outside the collapse target', () => { const toggle = vi.fn() const add = vi.fn() diff --git a/packages/emcn/src/components/collapsible-card/collapsible-card.tsx b/packages/emcn/src/components/collapsible-card/collapsible-card.tsx index 9104661a7ea..8bb9c460891 100644 --- a/packages/emcn/src/components/collapsible-card/collapsible-card.tsx +++ b/packages/emcn/src/components/collapsible-card/collapsible-card.tsx @@ -15,6 +15,8 @@ export interface CollapsibleCardProps badge?: React.ReactNode /** Header actions, outside the collapse target and arranged with standard spacing. */ actions?: React.ReactNode + /** Prevent header activation and remove it from tab order; independent actions stay available. */ + disabled?: boolean collapsed: boolean onToggleCollapse: () => void /** Animate expansion using the shared Expandable height transition. */ @@ -42,6 +44,7 @@ export function CollapsibleCard({ badge, actions, collapsed, + disabled = false, onToggleCollapse, animated = false, contentProps, @@ -58,16 +61,18 @@ export function CollapsibleCard({ <>
{ - if (event.target !== event.currentTarget) return + if (disabled || event.target !== event.currentTarget) return handleKeyboardActivation(event, onToggleCollapse) }} >