Skip to content

Commit 6aedeaf

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
improvement(ui): reuse collapsible cards for MCP tools
1 parent faa970e commit 6aedeaf

3 files changed

Lines changed: 96 additions & 56 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx‎

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
'use client'
22

33
import { useEffect, useRef, useState } from 'react'
4-
import { Badge, Button, Chip, ChipConfirmModal, cn, Tooltip, toast } from '@sim/emcn'
4+
import {
5+
Badge,
6+
Chip,
7+
ChipConfirmModal,
8+
CollapsibleCard,
9+
cn,
10+
OverflowText,
11+
Tooltip,
12+
toast,
13+
} from '@sim/emcn'
514
import { ArrowLeft, ChevronDown, Plus } from '@sim/emcn/icons'
615
import { createLogger } from '@sim/logger'
716
import { getErrorMessage } from '@sim/utils/errors'
@@ -535,59 +544,53 @@ export function MCP() {
535544
const requiredParams = tool.inputSchema?.required || []
536545

537546
return (
538-
<div
547+
<CollapsibleCard
539548
key={tool.name}
540-
className='overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-3)]'
541-
>
542-
<Button
543-
type='button'
544-
variant='ghost'
545-
onClick={() => hasParams && toggleToolExpanded(tool.name)}
546-
className={cn(
547-
'flex h-auto w-full items-start justify-between rounded-none px-2.5 py-2 text-left text-sm',
548-
hasParams && 'cursor-pointer hover-hover:bg-[var(--surface-4)]'
549-
)}
550-
disabled={!hasParams}
551-
>
552-
<div className='flex-1'>
553-
<div className='flex h-[16px] items-center gap-1.5'>
554-
<p className='text-[var(--text-primary)] text-sm leading-none'>
555-
{tool.name}
556-
</p>
557-
{issues.length > 0 && (
558-
<Tooltip.Root>
559-
<Tooltip.Trigger asChild>
560-
<div className='flex items-center'>
561-
<Badge variant={getIssueBadgeVariant(issues[0].issue)} size='sm'>
562-
{getIssueBadgeLabel(issues[0].issue)}
563-
</Badge>
564-
</div>
565-
</Tooltip.Trigger>
566-
<Tooltip.Content>
567-
Update in: {affectedWorkflows.join(', ')}
568-
</Tooltip.Content>
569-
</Tooltip.Root>
570-
)}
571-
</div>
549+
collapsed={!isExpanded || !hasParams}
550+
disabled={!hasParams}
551+
onToggleCollapse={() => toggleToolExpanded(tool.name)}
552+
title={
553+
<>
554+
<OverflowText
555+
label={tool.name}
556+
className='text-[var(--text-primary)]'
557+
focusTarget='nearest-interactive'
558+
/>
572559
{tool.description && (
573-
<p className='mt-1 text-[var(--text-tertiary)] text-sm'>
574-
{tool.description}
575-
</p>
560+
<span className='mt-1 block whitespace-normal'>{tool.description}</span>
576561
)}
577-
</div>
578-
{hasParams && (
579-
<ChevronDown
580-
className={cn(
581-
'mt-0.5 size-[14px] shrink-0 text-[var(--text-muted)] transition-transform duration-200',
582-
isExpanded && 'rotate-180'
583-
)}
584-
/>
585-
)}
586-
</Button>
587-
562+
</>
563+
}
564+
badge={
565+
<>
566+
{issues.length > 0 && (
567+
<Tooltip.Root>
568+
<Tooltip.Trigger asChild>
569+
<span className='flex shrink-0 items-center'>
570+
<Badge variant={getIssueBadgeVariant(issues[0].issue)} size='sm'>
571+
{getIssueBadgeLabel(issues[0].issue)}
572+
</Badge>
573+
</span>
574+
</Tooltip.Trigger>
575+
<Tooltip.Content>
576+
Update in: {affectedWorkflows.join(', ')}
577+
</Tooltip.Content>
578+
</Tooltip.Root>
579+
)}
580+
{hasParams && (
581+
<ChevronDown
582+
className={cn(
583+
'size-[14px] shrink-0 text-[var(--text-muted)] transition-transform duration-200',
584+
isExpanded && 'rotate-180'
585+
)}
586+
/>
587+
)}
588+
</>
589+
}
590+
>
588591
{isExpanded && hasParams && (
589-
<div className='border-[var(--border-1)] border-t bg-[var(--surface-2)] px-2.5 py-2'>
590-
<p className='mb-1.5 text-[var(--text-muted)] text-caption uppercase tracking-wide'>
592+
<>
593+
<p className='text-[var(--text-muted)] text-caption uppercase tracking-wide'>
591594
Parameters
592595
</p>
593596
<div className='flex flex-col gap-1.5'>
@@ -631,9 +634,9 @@ export function MCP() {
631634
}
632635
)}
633636
</div>
634-
</div>
637+
</>
635638
)}
636-
</div>
639+
</CollapsibleCard>
637640
)
638641
})}
639642
</div>

‎packages/emcn/src/components/collapsible-card/collapsible-card.test.tsx‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,38 @@ describe('CollapsibleCard', () => {
6262
}
6363
)
6464

65+
it('prevents disabled header activation without disabling independent actions', () => {
66+
const toggle = vi.fn()
67+
const action = vi.fn()
68+
mount(
69+
<CollapsibleCard
70+
title='Status'
71+
disabled
72+
collapsed
73+
onToggleCollapse={toggle}
74+
actions={
75+
<button type='button' onClick={action}>
76+
Refresh
77+
</button>
78+
}
79+
>
80+
Content
81+
</CollapsibleCard>
82+
)
83+
const trigger = container!.querySelector<HTMLElement>('[role="button"]')!
84+
expect(trigger.tabIndex).toBe(-1)
85+
expect(trigger.getAttribute('aria-disabled')).toBe('true')
86+
act(() => {
87+
trigger.click()
88+
trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
89+
trigger.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
90+
container!.querySelector<HTMLButtonElement>('button')!.click()
91+
})
92+
expect(toggle).not.toHaveBeenCalled()
93+
expect(action).toHaveBeenCalledTimes(1)
94+
expect(container!.textContent).not.toContain('Content')
95+
})
96+
6597
it('keeps enabled and disabled actions outside the collapse target', () => {
6698
const toggle = vi.fn()
6799
const add = vi.fn()

‎packages/emcn/src/components/collapsible-card/collapsible-card.tsx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface CollapsibleCardProps
1515
badge?: React.ReactNode
1616
/** Header actions, outside the collapse target and arranged with standard spacing. */
1717
actions?: React.ReactNode
18+
/** Prevent header activation and remove it from tab order; independent actions stay available. */
19+
disabled?: boolean
1820
collapsed: boolean
1921
onToggleCollapse: () => void
2022
/** Animate expansion using the shared Expandable height transition. */
@@ -42,6 +44,7 @@ export function CollapsibleCard({
4244
badge,
4345
actions,
4446
collapsed,
47+
disabled = false,
4548
onToggleCollapse,
4649
animated = false,
4750
contentProps,
@@ -58,16 +61,18 @@ export function CollapsibleCard({
5861
<>
5962
<div
6063
role='button'
61-
tabIndex={0}
64+
tabIndex={disabled ? -1 : 0}
6265
aria-expanded={!collapsed}
66+
aria-disabled={disabled || undefined}
6367
aria-controls={contentProps?.id}
6468
className={cn(
65-
'flex min-w-0 flex-1 cursor-pointer items-center gap-2 px-2.5 py-[5px]',
69+
'flex min-w-0 flex-1 items-center gap-2 px-2.5 py-[5px]',
70+
!disabled && 'cursor-pointer',
6671
actions && 'pr-2'
6772
)}
68-
onClick={onToggleCollapse}
73+
onClick={disabled ? undefined : onToggleCollapse}
6974
onKeyDown={(event) => {
70-
if (event.target !== event.currentTarget) return
75+
if (disabled || event.target !== event.currentTarget) return
7176
handleKeyboardActivation(event, onToggleCollapse)
7277
}}
7378
>

0 commit comments

Comments
 (0)