Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { ComponentType } from 'react'
import { memo } from 'react'
import { Command } from 'cmdk'
import { ChevronRight } from 'lucide-react'
import { File, Workflow } from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
import type { CommandItemProps } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
Expand Down Expand Up @@ -328,6 +329,43 @@ export const MemoizedPageItem = memo(
prev.query === next.query
)

export const MemoizedCategoryItem = memo(
function CategoryItem({
value,
onSelect,
icon: Icon,
name,
count,
query,
}: {
value: string
onSelect: () => void
icon: ComponentType<{ className?: string }>
name: string
count: number
query?: string
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate text-[var(--text-body)]'>
<HighlightedText text={name} query={query} />
</span>
<span className='ml-auto flex flex-shrink-0 items-center gap-1.5 text-[var(--text-subtle)] text-small'>
{count}
<ChevronRight className='size-[14px]' />
</span>
</Command.Item>
)
},
(prev, next) =>
prev.value === next.value &&
prev.icon === next.icon &&
prev.name === next.name &&
prev.count === next.count &&
prev.query === next.query
)

export const MemoizedIconItem = memo(
function IconItem({
value,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
HighlightedText,
MemoizedActionItem,
MemoizedCategoryItem,
MemoizedCommandItem,
MemoizedFileItem,
MemoizedIconItem,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
export type { RecentRenderItem } from './search-groups'
export {
ActionsGroup,
BlocksGroup,
BrowseGroup,
ChatsGroup,
ConnectedAccountsGroup,
DocsGroup,
FilesGroup,
IntegrationsGroup,
KnowledgeBasesGroup,
PagesGroup,
RecentsGroup,
TablesGroup,
ToolOpsGroup,
ToolsGroup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,29 @@
import type { ComponentType } from 'react'
import { memo } from 'react'
import { Command } from 'cmdk'
import {
Activity,
BarChart3,
Blocks,
FileText,
GitBranch,
LifeBuoy,
ListChecks,
Mail,
Megaphone,
MessageCircle,
Search as SearchIcon,
Shield,
ShoppingCart,
Sparkles,
TrendingUp,
Users,
Zap,
} from 'lucide-react'
import { Database, Table } from '@/components/emcn/icons'
import {
MemoizedActionItem,
MemoizedCategoryItem,
MemoizedCommandItem,
MemoizedFileItem,
MemoizedIconItem,
Expand All @@ -24,12 +44,44 @@ import type {
WorkspaceItem,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
import { GROUP_HEADING_CLASSNAME } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
import { IntegrationType } from '@/blocks/types'
import type {
SearchBlockItem,
SearchCategory,
SearchDocItem,
SearchToolOperationItem,
} from '@/stores/modals/search/types'

/**
* Icon per integration category. Exhaustive over {@link IntegrationType} so a
* newly added category is a compile error here rather than a silent fallback.
*/
const INTEGRATION_CATEGORY_ICONS: Record<IntegrationType, ComponentType<{ className?: string }>> = {
[IntegrationType.AI]: Sparkles,
[IntegrationType.Analytics]: BarChart3,
[IntegrationType.Commerce]: ShoppingCart,
[IntegrationType.Communication]: MessageCircle,
[IntegrationType.Databases]: Database,
[IntegrationType.DevOps]: GitBranch,
[IntegrationType.Documents]: FileText,
[IntegrationType.Email]: Mail,
[IntegrationType.HR]: Users,
[IntegrationType.Marketing]: Megaphone,
[IntegrationType.Observability]: Activity,
[IntegrationType.Productivity]: ListChecks,
[IntegrationType.Sales]: TrendingUp,
[IntegrationType.Search]: SearchIcon,
[IntegrationType.Security]: Shield,
[IntegrationType.Support]: LifeBuoy,
}

/** Resolves the icon for a browse category from its kind, then its integration slug. */
function categoryIcon(category: SearchCategory): ComponentType<{ className?: string }> {
if (category.kind === 'block') return Blocks
if (category.kind === 'trigger') return Zap
return INTEGRATION_CATEGORY_ICONS[category.id as IntegrationType] ?? Blocks
}

export const ActionsGroup = memo(function ActionsGroup({
items,
onSelect,
Expand Down Expand Up @@ -57,18 +109,72 @@ export const ActionsGroup = memo(function ActionsGroup({
)
})

/** A recent selection resolved to a renderable row by the modal. */
export interface RecentRenderItem {
id: string
label: string
icon: ComponentType<{ className?: string }>
bgColor: string
onSelect: () => void
}

export const RecentsGroup = memo(function RecentsGroup({ items }: { items: RecentRenderItem[] }) {
if (items.length === 0) return null
return (
<Command.Group heading='Recent' className={GROUP_HEADING_CLASSNAME}>
Comment thread
waleedlatif1 marked this conversation as resolved.
{items.map((item) => (
<MemoizedCommandItem
key={item.id}
value={`${item.label} recent-${item.id}`}
onSelect={item.onSelect}
icon={item.icon}
bgColor={item.bgColor}
showColoredIcon
label={item.label}
/>
))}
</Command.Group>
)
})

export const BrowseGroup = memo(function BrowseGroup({
items,
onSelect,
}: {
items: SearchCategory[]
onSelect: (category: SearchCategory) => void
}) {
if (items.length === 0) return null
return (
<Command.Group heading='Browse' className={GROUP_HEADING_CLASSNAME}>
{items.map((category) => (
<MemoizedCategoryItem
key={category.id}
value={`${category.label} category-${category.id}`}
onSelect={() => onSelect(category)}
icon={categoryIcon(category)}
name={category.label}
count={category.count}
/>
))}
</Command.Group>
)
})

export const BlocksGroup = memo(function BlocksGroup({
items,
onSelect,
query,
heading = 'Blocks',
}: {
items: SearchBlockItem[]
onSelect: (block: SearchBlockItem) => void
query?: string
heading?: string
}) {
if (items.length === 0) return null
return (
<Command.Group heading='Blocks' className={GROUP_HEADING_CLASSNAME}>
<Command.Group heading={heading} className={GROUP_HEADING_CLASSNAME}>
{items.map((block) => (
<MemoizedCommandItem
key={block.id}
Expand All @@ -89,14 +195,16 @@ export const ToolsGroup = memo(function ToolsGroup({
items,
onSelect,
query,
heading = 'Tools',
}: {
items: SearchBlockItem[]
onSelect: (tool: SearchBlockItem) => void
query?: string
heading?: string
}) {
if (items.length === 0) return null
return (
<Command.Group heading='Tools' className={GROUP_HEADING_CLASSNAME}>
<Command.Group heading={heading} className={GROUP_HEADING_CLASSNAME}>
{items.map((tool) => (
<MemoizedCommandItem
key={tool.id}
Expand All @@ -117,14 +225,16 @@ export const TriggersGroup = memo(function TriggersGroup({
items,
onSelect,
query,
heading = 'Triggers',
}: {
items: SearchBlockItem[]
onSelect: (trigger: SearchBlockItem) => void
query?: string
heading?: string
}) {
if (items.length === 0) return null
return (
<Command.Group heading='Triggers' className={GROUP_HEADING_CLASSNAME}>
<Command.Group heading={heading} className={GROUP_HEADING_CLASSNAME}>
{items.map((trigger) => (
<MemoizedCommandItem
key={trigger.id}
Expand Down
Loading
Loading