Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .claude/commands/add-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ When the user asks you to create a block:
```typescript
import { {ServiceName}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'

export const {ServiceName}Block: BlockConfig = {
Expand All @@ -29,6 +29,8 @@ export const {ServiceName}Block: BlockConfig = {
longDescription: 'Detailed description for docs',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools', // 'tools' | 'blocks' | 'triggers'
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
bgColor: '#HEXCOLOR', // Brand color
icon: {ServiceName}Icon,

Expand Down Expand Up @@ -629,7 +631,7 @@ export const registry: Record<string, BlockConfig> = {
```typescript
import { ServiceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'

export const ServiceBlock: BlockConfig = {
Expand All @@ -639,6 +641,8 @@ export const ServiceBlock: BlockConfig = {
longDescription: 'Full description for documentation...',
docsLink: 'https://docs.sim.ai/tools/service',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['oauth', 'api'],
bgColor: '#FF6B6B',
icon: ServiceIcon,
authMode: AuthMode.OAuth,
Expand Down Expand Up @@ -796,6 +800,8 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU

## Checklist Before Finishing

- [ ] `integrationType` is set to the correct `IntegrationType` enum value
- [ ] `tags` array includes all applicable `IntegrationTag` values
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
- [ ] Conditions use correct syntax (field, value, not, and)
- [ ] DependsOn set for fields that need other values
Expand Down
6 changes: 5 additions & 1 deletion .claude/commands/add-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
```typescript
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'

export const {Service}Block: BlockConfig = {
Expand All @@ -123,6 +123,8 @@ export const {Service}Block: BlockConfig = {
longDescription: '...',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools',
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
bgColor: '#HEXCOLOR',
icon: {Service}Icon,
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
Expand Down Expand Up @@ -410,6 +412,8 @@ If creating V2 versions (API-aligned outputs):

### Block
- [ ] Created `blocks/blocks/{service}.ts`
- [ ] Set `integrationType` to the correct `IntegrationType` enum value
- [ ] Set `tags` array with all applicable `IntegrationTag` values
- [ ] Defined operation dropdown with all operations
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
- [ ] Added conditional fields per operation
Expand Down
140 changes: 109 additions & 31 deletions apps/sim/app/(landing)/integrations/components/integration-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,132 @@ import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mappi
import type { Integration } from '@/app/(landing)/integrations/data/types'
import { IntegrationCard } from './integration-card'

const CATEGORY_LABELS: Record<string, string> = {
ai: 'AI',
analytics: 'Analytics',
automation: 'Automation',
communication: 'Communication',
crm: 'CRM',
'customer-support': 'Customer Support',
databases: 'Databases',
design: 'Design',
'developer-tools': 'Developer Tools',
documents: 'Documents',
ecommerce: 'E-commerce',
email: 'Email',
'file-storage': 'File Storage',
hr: 'HR',
media: 'Media',
productivity: 'Productivity',
'sales-intelligence': 'Sales Intelligence',
search: 'Search',
security: 'Security',
social: 'Social',
other: 'Other',
} as const

interface IntegrationGridProps {
integrations: Integration[]
}

export function IntegrationGrid({ integrations }: IntegrationGridProps) {
const [query, setQuery] = useState('')
const [activeCategory, setActiveCategory] = useState<string | null>(null)

const availableCategories = useMemo(() => {
const counts = new Map<string, number>()
for (const i of integrations) {
if (i.integrationType) {
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
}
}
return Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.map(([key]) => key)
}, [integrations])

const filtered = useMemo(() => {
let results = integrations

if (activeCategory) {
results = results.filter((i) => i.integrationType === activeCategory)
}

const q = query.trim().toLowerCase()
if (!q) return integrations
return integrations.filter(
(i) =>
i.name.toLowerCase().includes(q) ||
i.description.toLowerCase().includes(q) ||
i.operations.some(
(op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
) ||
i.triggers.some((t) => t.name.toLowerCase().includes(q))
)
}, [integrations, query])
if (q) {
results = results.filter(
(i) =>
i.name.toLowerCase().includes(q) ||
i.description.toLowerCase().includes(q) ||
i.operations.some(
(op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
) ||
i.triggers.some((t) => t.name.toLowerCase().includes(q))
)
}

return results
}, [integrations, query, activeCategory])

return (
<div>
<div className='relative mb-8 max-w-[480px]'>
<svg
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
<div className='mb-6 flex flex-col gap-4 sm:flex-row sm:items-center'>
<div className='relative max-w-[480px] flex-1'>
<svg
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<circle cx={11} cy={11} r={8} />
<path d='m21 21-4.35-4.35' />
</svg>
<Input
type='search'
placeholder='Search integrations, tools, or triggers…'
value={query}
onChange={(e) => setQuery(e.target.value)}
className='pl-9'
aria-label='Search integrations'
/>
</div>
</div>

<div className='mb-8 flex flex-wrap gap-2'>
<button
type='button'
onClick={() => setActiveCategory(null)}
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
activeCategory === null
? 'border-[#555] bg-[#333] text-[#ECECEC]'
: 'border-[#2A2A2A] bg-transparent text-[#999] hover:border-[#3d3d3d] hover:text-[#ECECEC]'
}`}
>
<circle cx={11} cy={11} r={8} />
<path d='m21 21-4.35-4.35' />
</svg>
<Input
type='search'
placeholder='Search integrations, tools, or triggers…'
value={query}
onChange={(e) => setQuery(e.target.value)}
className='pl-9'
aria-label='Search integrations'
/>
All
</button>
{availableCategories.map((cat) => (
<button
key={cat}
type='button'
onClick={() => setActiveCategory(activeCategory === cat ? null : cat)}
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
activeCategory === cat
? 'border-[#555] bg-[#333] text-[#ECECEC]'
: 'border-[#2A2A2A] bg-transparent text-[#999] hover:border-[#3d3d3d] hover:text-[#ECECEC]'
}`}
>
{CATEGORY_LABELS[cat] || cat}
</button>
))}
</div>

{filtered.length === 0 ? (
<p className='py-12 text-center text-[#555] text-[15px]'>
No integrations found for &ldquo;{query}&rdquo;
No integrations found
{query ? <> for &ldquo;{query}&rdquo;</> : null}
{activeCategory ? <> in {CATEGORY_LABELS[activeCategory] || activeCategory}</> : null}
</p>
) : (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
Expand Down
Loading
Loading