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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useCallback, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'
import {
Button,
DropdownMenu,
Expand All @@ -13,7 +13,7 @@ import {
DropdownMenuTrigger,
Tooltip,
} from '@/components/emcn'
import { Plus } from '@/components/emcn/icons'
import { Folder, Plus } from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import {
Expand Down Expand Up @@ -68,6 +68,8 @@ export function useAvailableResources(
id: w.id,
name: w.name,
color: w.color,
folderId: w.folderId ?? null,
sortOrder: w.sortOrder,
isOpen: existingKeys.has(`workflow:${w.id}`),
})),
},
Expand All @@ -76,6 +78,8 @@ export function useAvailableResources(
items: folders.map((f) => ({
id: f.id,
name: f.name,
parentId: f.parentId ?? null,
sortOrder: f.sortOrder,
isOpen: existingKeys.has(`folder:${f.id}`),
})),
},
Expand Down Expand Up @@ -116,6 +120,104 @@ export function useAvailableResources(
}, [workflows, folders, tables, files, knowledgeBases, tasks, existingKeys, excludeTypes])
}

export type WorkflowTreeNode =
| { kind: 'workflow'; id: string; name: string; color: string; isOpen?: boolean }
| { kind: 'folder'; id: string; name: string; children: WorkflowTreeNode[] }

export function buildWorkflowFolderTree(
workflowItems: AvailableItem[],
folderItems: AvailableItem[]
): WorkflowTreeNode[] {
const knownFolderIds = new Set(folderItems.map((f) => f.id))

const byFolder = new Map<string | null, AvailableItem[]>()
for (const w of workflowItems) {
const fid = (w.folderId as string | null | undefined) ?? null
const key = fid && knownFolderIds.has(fid) ? fid : null
const bucket = byFolder.get(key) ?? []
bucket.push(w)
byFolder.set(key, bucket)
}

const toWorkflowNode = (w: AvailableItem): WorkflowTreeNode => ({
kind: 'workflow',
id: w.id,
name: w.name,
color: (w.color as string) ?? '#808080',
isOpen: w.isOpen,
})

const buildLevel = (parentId: string | null): WorkflowTreeNode[] => {
const childFolders = folderItems.filter(
(f) => ((f.parentId as string | null | undefined) ?? null) === parentId
)
const childWorkflows = byFolder.get(parentId) ?? []

const mixed: Array<{ sortOrder: number; id: string; node: WorkflowTreeNode }> = []

for (const f of childFolders) {
const children = buildLevel(f.id)
if (children.length === 0) continue
mixed.push({
Comment thread
waleedlatif1 marked this conversation as resolved.
sortOrder: (f.sortOrder as number) ?? 0,
id: f.id,
node: { kind: 'folder', id: f.id, name: f.name, children },
})
}

for (const w of childWorkflows) {
mixed.push({
sortOrder: (w.sortOrder as number) ?? 0,
id: w.id,
node: toWorkflowNode(w),
})
}

mixed.sort((a, b) =>
a.sortOrder !== b.sortOrder ? a.sortOrder - b.sortOrder : a.id.localeCompare(b.id)
)
Comment thread
waleedlatif1 marked this conversation as resolved.
return mixed.map((m) => m.node)
}

return buildLevel(null)
}

interface WorkflowFolderTreeItemsProps {
nodes: WorkflowTreeNode[]
onSelect: (resource: MothershipResource, isOpen?: boolean) => void
}

export function WorkflowFolderTreeItems({ nodes, onSelect }: WorkflowFolderTreeItemsProps) {
return (
<>
{nodes.map((node) =>
node.kind === 'workflow' ? (
<DropdownMenuItem
key={node.id}
onClick={() =>
onSelect({ type: 'workflow', id: node.id, title: node.name }, node.isOpen)
}
>
{getResourceConfig('workflow').renderDropdownItem({
item: { id: node.id, name: node.name, color: node.color },
})}
</DropdownMenuItem>
) : (
<DropdownMenuSub key={node.id}>
<DropdownMenuSubTrigger>
<Folder className='h-[14px] w-[14px]' />
<span>{node.name}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<WorkflowFolderTreeItems nodes={node.children} onSelect={onSelect} />
</DropdownMenuSubContent>
</DropdownMenuSub>
)
)}
</>
)
}

export function AddResourceDropdown({
workspaceId,
existingKeys,
Expand All @@ -128,27 +230,30 @@ export function AddResourceDropdown({
const [activeIndex, setActiveIndex] = useState(0)
const available = useAvailableResources(workspaceId, existingKeys, excludeTypes)

const handleOpenChange = useCallback((next: boolean) => {
const handleOpenChange = (next: boolean) => {
setOpen(next)
if (!next) {
setSearch('')
setActiveIndex(0)
}
}, [])
}

const select = useCallback(
(resource: MothershipResource, isOpen?: boolean) => {
if (isOpen && onSwitch) {
onSwitch(resource.id)
} else {
onAdd(resource)
}
setOpen(false)
setSearch('')
setActiveIndex(0)
},
[onAdd, onSwitch]
)
const select = (resource: MothershipResource, isOpen?: boolean) => {
if (isOpen && onSwitch) {
onSwitch(resource.id)
} else {
onAdd(resource)
}
setOpen(false)
setSearch('')
setActiveIndex(0)
}

const workflowTree = useMemo(() => {
const workflowGroup = available.find((g) => g.type === 'workflow')
const folderGroup = available.find((g) => g.type === 'folder')
return buildWorkflowFolderTree(workflowGroup?.items ?? [], folderGroup?.items ?? [])
}, [available])

const filtered = useMemo(() => {
const q = search.toLowerCase().trim()
Expand All @@ -158,25 +263,22 @@ export function AddResourceDropdown({
)
}, [search, available])

const handleSearchKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (!filtered) return
if (e.key === 'ArrowDown') {
const handleSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!filtered) return
if (e.key === 'ArrowDown') {
e.preventDefault()
setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1))
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setActiveIndex((prev) => Math.max(prev - 1, 0))
} else if (e.key === 'Enter' || (e.key === 'Tab' && !e.shiftKey)) {
if (filtered.length > 0 && filtered[activeIndex]) {
e.preventDefault()
setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1))
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setActiveIndex((prev) => Math.max(prev - 1, 0))
} else if (e.key === 'Enter' || (e.key === 'Tab' && !e.shiftKey)) {
if (filtered.length > 0 && filtered[activeIndex]) {
e.preventDefault()
const { type, item } = filtered[activeIndex]
select({ type, id: item.id, title: item.name }, item.isOpen)
}
const { type, item } = filtered[activeIndex]
select({ type, id: item.id, title: item.name }, item.isOpen)
}
},
[filtered, activeIndex, select]
)
}
}

return (
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
Expand All @@ -199,7 +301,7 @@ export function AddResourceDropdown({
<DropdownMenuContent
align='start'
sideOffset={8}
className='flex w-[240px] flex-col overflow-hidden'
className='flex w-[320px] flex-col overflow-hidden'
onCloseAutoFocus={(e) => e.preventDefault()}
>
<DropdownMenuSearchInput
Expand All @@ -224,9 +326,6 @@ export function AddResourceDropdown({
onClick={() => select({ type, id: item.id, title: item.name }, item.isOpen)}
>
{config.renderDropdownItem({ item })}
<span className='ml-auto pl-2 text-[var(--text-tertiary)] text-xs'>
{config.label}
</span>
</DropdownMenuItem>
)
})
Expand All @@ -237,25 +336,33 @@ export function AddResourceDropdown({
)
) : (
<>
{workflowTree.length > 0 && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<div
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]'
style={{
backgroundColor: '#808080',
borderColor: '#80808060',
backgroundClip: 'padding-box',
}}
/>
<span>Workflows</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<WorkflowFolderTreeItems nodes={workflowTree} onSelect={select} />
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
{available.map(({ type, items }) => {
if (type === 'workflow' || type === 'folder') return null
if (items.length === 0) return null
const config = getResourceConfig(type)
const Icon = config.icon
return (
<DropdownMenuSub key={type}>
<DropdownMenuSubTrigger>
{type === 'workflow' ? (
<div
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]'
style={{
backgroundColor: '#808080',
borderColor: '#80808060',
backgroundClip: 'padding-box',
}}
/>
) : (
<Icon className='h-[14px] w-[14px]' />
)}
<Icon className='h-[14px] w-[14px]' />
<span>{config.label}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
export type { AddResourceDropdownProps, AvailableItem } from './add-resource-dropdown'
export { AddResourceDropdown, useAvailableResources } from './add-resource-dropdown'
export type {
AddResourceDropdownProps,
AvailableItem,
WorkflowTreeNode,
} from './add-resource-dropdown'
export {
AddResourceDropdown,
buildWorkflowFolderTree,
useAvailableResources,
WorkflowFolderTreeItems,
} from './add-resource-dropdown'
Loading
Loading