mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(sidebar): context menu for nav items in sidebar, toolbar blocks, added missing docs for various blocks and triggers (#2754)
* feat(sidebar): context menu for nav items in sidebar * added toolbar context menu, fixed incorrect access pattern in old context menus and added docs for missing blocks * fixed links
This commit is contained in:
@@ -95,7 +95,12 @@ export function ChunkContextMenu({
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
||||
@@ -100,7 +100,12 @@ export function DocumentContextMenu({
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
||||
@@ -99,7 +99,12 @@ export function KnowledgeBaseContextMenu({
|
||||
disableDelete = false,
|
||||
}: KnowledgeBaseContextMenuProps) {
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
||||
@@ -43,7 +43,12 @@ export function KnowledgeListContextMenu({
|
||||
disableAdd = false,
|
||||
}: KnowledgeListContextMenuProps) {
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
||||
@@ -44,7 +44,7 @@ export function SnapshotContextMenu({
|
||||
return createPortal(
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onClose}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
|
||||
@@ -47,7 +47,12 @@ export function LogRowContextMenu({
|
||||
const hasWorkflow = Boolean(log?.workflow?.id || log?.workflowId)
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
||||
@@ -56,7 +56,7 @@ export function BlockContextMenu({
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onClose}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
|
||||
@@ -38,7 +38,7 @@ export function PaneContextMenu({
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onClose}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
|
||||
@@ -673,7 +673,7 @@ function WorkflowInputMapperSyncWrapper({
|
||||
|
||||
if (!workflowId) {
|
||||
return (
|
||||
<div className='rounded-md border border-gray-600/50 bg-gray-900/20 p-4 text-center text-gray-400 text-sm'>
|
||||
<div className='rounded-md border border-[var(--border-1)] border-dashed bg-[var(--surface-3)] p-4 text-center text-[var(--text-muted)] text-sm'>
|
||||
Select a workflow to configure its inputs
|
||||
</div>
|
||||
)
|
||||
@@ -681,15 +681,15 @@ function WorkflowInputMapperSyncWrapper({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='flex items-center justify-center rounded-md border border-gray-600/50 bg-gray-900/20 p-8'>
|
||||
<Loader2 className='h-5 w-5 animate-spin text-gray-400' />
|
||||
<div className='flex items-center justify-center rounded-md border border-[var(--border-1)] border-dashed bg-[var(--surface-3)] p-8'>
|
||||
<Loader2 className='h-5 w-5 animate-spin text-[var(--text-muted)]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (inputFields.length === 0) {
|
||||
return (
|
||||
<div className='rounded-md border border-gray-600/50 bg-gray-900/20 p-4 text-center text-gray-400 text-sm'>
|
||||
<div className='rounded-md border border-[var(--border-1)] border-dashed bg-[var(--surface-3)] p-4 text-center text-[var(--text-muted)] text-sm'>
|
||||
This workflow has no custom input fields
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { BookOpen, Check, ChevronUp, Pencil, RepeatIcon, Settings, SplitIcon } from 'lucide-react'
|
||||
import { BookOpen, Check, ChevronUp, Pencil, Settings } from 'lucide-react'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import {
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
useEditorBlockProperties,
|
||||
useEditorSubblockLayout,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks'
|
||||
import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config'
|
||||
import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
|
||||
import { getSubBlockStableKey } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils'
|
||||
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
@@ -58,9 +60,8 @@ export function Editor() {
|
||||
const isSubflow =
|
||||
currentBlock && (currentBlock.type === 'loop' || currentBlock.type === 'parallel')
|
||||
|
||||
// Get subflow display properties
|
||||
const subflowIcon = isSubflow && currentBlock.type === 'loop' ? RepeatIcon : SplitIcon
|
||||
const subflowBgColor = isSubflow && currentBlock.type === 'loop' ? '#2FB3FF' : '#FEE12B'
|
||||
// Get subflow display properties from configs
|
||||
const subflowConfig = isSubflow ? (currentBlock.type === 'loop' ? LoopTool : ParallelTool) : null
|
||||
|
||||
// Refs for resize functionality
|
||||
const subBlocksRef = useRef<HTMLDivElement>(null)
|
||||
@@ -176,8 +177,9 @@ export function Editor() {
|
||||
* Handles opening documentation link in a new secure tab.
|
||||
*/
|
||||
const handleOpenDocs = () => {
|
||||
if (blockConfig?.docsLink) {
|
||||
window.open(blockConfig.docsLink, '_blank', 'noopener,noreferrer')
|
||||
const docsLink = isSubflow ? subflowConfig?.docsLink : blockConfig?.docsLink
|
||||
if (docsLink) {
|
||||
window.open(docsLink, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,10 +197,10 @@ export function Editor() {
|
||||
{(blockConfig || isSubflow) && currentBlock?.type !== 'note' && (
|
||||
<div
|
||||
className='flex h-[18px] w-[18px] items-center justify-center rounded-[4px]'
|
||||
style={{ background: isSubflow ? subflowBgColor : blockConfig?.bgColor }}
|
||||
style={{ background: isSubflow ? subflowConfig?.bgColor : blockConfig?.bgColor }}
|
||||
>
|
||||
<IconComponent
|
||||
icon={isSubflow ? subflowIcon : blockConfig?.icon}
|
||||
icon={isSubflow ? subflowConfig?.icon : blockConfig?.icon}
|
||||
className='h-[12px] w-[12px] text-[var(--white)]'
|
||||
/>
|
||||
</div>
|
||||
@@ -295,7 +297,7 @@ export function Editor() {
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
{currentBlock && !isSubflow && blockConfig?.docsLink && (
|
||||
{currentBlock && (isSubflow ? subflowConfig?.docsLink : blockConfig?.docsLink) && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { createDragPreview, type DragItemInfo } from './drag-preview'
|
||||
export { ToolbarItemContextMenu } from './toolbar-item-context-menu'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { ToolbarItemContextMenu } from './toolbar-item-context-menu'
|
||||
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
|
||||
interface ToolbarItemContextMenuProps {
|
||||
/**
|
||||
* Whether the context menu is open
|
||||
*/
|
||||
isOpen: boolean
|
||||
/**
|
||||
* Position of the context menu
|
||||
*/
|
||||
position: { x: number; y: number }
|
||||
/**
|
||||
* Ref for the menu element
|
||||
*/
|
||||
menuRef: React.RefObject<HTMLDivElement | null>
|
||||
/**
|
||||
* Callback when menu should close
|
||||
*/
|
||||
onClose: () => void
|
||||
/**
|
||||
* Callback when add to canvas is clicked
|
||||
*/
|
||||
onAddToCanvas: () => void
|
||||
/**
|
||||
* Callback when view documentation is clicked
|
||||
*/
|
||||
onViewDocumentation?: () => void
|
||||
/**
|
||||
* Whether the view documentation option should be shown
|
||||
*/
|
||||
showViewDocumentation?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu component for toolbar items (triggers and blocks).
|
||||
* Displays options to add to canvas and view documentation.
|
||||
*/
|
||||
export function ToolbarItemContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
menuRef,
|
||||
onClose,
|
||||
onAddToCanvas,
|
||||
onViewDocumentation,
|
||||
showViewDocumentation = false,
|
||||
}: ToolbarItemContextMenuProps) {
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
}}
|
||||
/>
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onAddToCanvas()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Add to canvas
|
||||
</PopoverItem>
|
||||
{showViewDocumentation && onViewDocumentation && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onViewDocumentation()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
View documentation
|
||||
</PopoverItem>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getTriggersForSidebar,
|
||||
hasTriggerCapability,
|
||||
} from '@/lib/workflows/triggers/trigger-utils'
|
||||
import { ToolbarItemContextMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/components'
|
||||
import {
|
||||
calculateTriggerHeights,
|
||||
useToolbarItemInteractions,
|
||||
@@ -34,6 +35,7 @@ interface BlockItem {
|
||||
config?: BlockConfig
|
||||
icon?: any
|
||||
bgColor?: string
|
||||
docsLink?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,6 +100,7 @@ function getBlocks() {
|
||||
type: LoopTool.type,
|
||||
icon: LoopTool.icon,
|
||||
bgColor: LoopTool.bgColor,
|
||||
docsLink: LoopTool.docsLink,
|
||||
isSpecial: true,
|
||||
})
|
||||
|
||||
@@ -106,6 +109,7 @@ function getBlocks() {
|
||||
type: ParallelTool.type,
|
||||
icon: ParallelTool.icon,
|
||||
bgColor: ParallelTool.bgColor,
|
||||
docsLink: ParallelTool.docsLink,
|
||||
isSpecial: true,
|
||||
})
|
||||
|
||||
@@ -178,6 +182,16 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
|
||||
// Toggle animation state
|
||||
const [isToggling, setIsToggling] = useState(false)
|
||||
|
||||
// Context menu state
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||
const [activeItemInfo, setActiveItemInfo] = useState<{
|
||||
type: string
|
||||
isTrigger: boolean
|
||||
docsLink?: string
|
||||
} | null>(null)
|
||||
|
||||
// Toolbar store
|
||||
const { toolbarTriggersHeight, setToolbarTriggersHeight, preSearchHeight, setPreSearchHeight } =
|
||||
useToolbarStore()
|
||||
@@ -338,6 +352,68 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
|
||||
setIsToggling(false)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Handle context menu for toolbar items
|
||||
*/
|
||||
const handleItemContextMenu = useCallback(
|
||||
(e: React.MouseEvent, type: string, isTrigger: boolean, docsLink?: string) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY })
|
||||
setActiveItemInfo({ type, isTrigger, docsLink })
|
||||
setIsContextMenuOpen(true)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
/**
|
||||
* Close context menu and clear active item state
|
||||
*/
|
||||
const closeContextMenu = useCallback(() => {
|
||||
setIsContextMenuOpen(false)
|
||||
setActiveItemInfo(null)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Handle add to canvas from context menu
|
||||
*/
|
||||
const handleContextMenuAddToCanvas = useCallback(() => {
|
||||
if (activeItemInfo) {
|
||||
handleItemClick(activeItemInfo.type, activeItemInfo.isTrigger)
|
||||
}
|
||||
}, [activeItemInfo, handleItemClick])
|
||||
|
||||
/**
|
||||
* Handle view documentation from context menu
|
||||
*/
|
||||
const handleViewDocumentation = useCallback(() => {
|
||||
if (activeItemInfo?.docsLink) {
|
||||
window.open(activeItemInfo.docsLink, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}, [activeItemInfo])
|
||||
|
||||
/**
|
||||
* Handle clicks outside the context menu to close it
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isContextMenuOpen) return
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) {
|
||||
closeContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
}, 0)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
}, [isContextMenuOpen, closeContextMenu])
|
||||
|
||||
/**
|
||||
* Handle keyboard navigation with ArrowUp / ArrowDown when the toolbar tab
|
||||
* is active and search is open (e.g. after Mod+F). Navigation order:
|
||||
@@ -553,6 +629,9 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
|
||||
})
|
||||
}}
|
||||
onClick={() => handleItemClick(trigger.type, isTriggerCapable)}
|
||||
onContextMenu={(e) =>
|
||||
handleItemContextMenu(e, trigger.type, isTriggerCapable, trigger.docsLink)
|
||||
}
|
||||
className={clsx(
|
||||
'group flex h-[28px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]',
|
||||
'cursor-pointer hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
|
||||
@@ -642,6 +721,14 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
|
||||
document.body.classList.remove('sim-drag-subflow')
|
||||
}}
|
||||
onClick={() => handleItemClick(block.type, false)}
|
||||
onContextMenu={(e) =>
|
||||
handleItemContextMenu(
|
||||
e,
|
||||
block.type,
|
||||
false,
|
||||
block.docsLink ?? block.config?.docsLink
|
||||
)
|
||||
}
|
||||
className={clsx(
|
||||
'group flex h-[28px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]',
|
||||
'cursor-pointer hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
|
||||
@@ -685,6 +772,17 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar Item Context Menu */}
|
||||
<ToolbarItemContextMenu
|
||||
isOpen={isContextMenuOpen}
|
||||
position={contextMenuPosition}
|
||||
menuRef={contextMenuRef}
|
||||
onClose={closeContextMenu}
|
||||
onAddToCanvas={handleContextMenuAddToCanvas}
|
||||
onViewDocumentation={handleViewDocumentation}
|
||||
showViewDocumentation={Boolean(activeItemInfo?.docsLink)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -9,4 +9,5 @@ export const LoopTool = {
|
||||
name: 'Loop',
|
||||
icon: RepeatIcon,
|
||||
bgColor: '#2FB3FF',
|
||||
docsLink: 'https://docs.sim.ai/blocks/loop',
|
||||
} as const
|
||||
|
||||
@@ -9,4 +9,5 @@ export const ParallelTool = {
|
||||
name: 'Parallel',
|
||||
icon: SplitIcon,
|
||||
bgColor: '#FEE12B',
|
||||
docsLink: 'https://docs.sim.ai/blocks/parallel',
|
||||
} as const
|
||||
|
||||
@@ -66,7 +66,7 @@ export function LogRowContextMenu({
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onClose}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
|
||||
@@ -52,7 +52,7 @@ export function OutputContextMenu({
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onClose}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { HelpModal } from './help-modal/help-modal'
|
||||
export { NavItemContextMenu } from './nav-item-context-menu'
|
||||
export { SearchModal } from './search-modal/search-modal'
|
||||
export { SettingsModal } from './settings-modal/settings-modal'
|
||||
export { UsageIndicator } from './usage-indicator/usage-indicator'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { NavItemContextMenu } from './nav-item-context-menu'
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
|
||||
interface NavItemContextMenuProps {
|
||||
/**
|
||||
* Whether the context menu is open
|
||||
*/
|
||||
isOpen: boolean
|
||||
/**
|
||||
* Position of the context menu
|
||||
*/
|
||||
position: { x: number; y: number }
|
||||
/**
|
||||
* Ref for the menu element
|
||||
*/
|
||||
menuRef: React.RefObject<HTMLDivElement | null>
|
||||
/**
|
||||
* Callback when menu should close
|
||||
*/
|
||||
onClose: () => void
|
||||
/**
|
||||
* Callback when open in new tab is clicked
|
||||
*/
|
||||
onOpenInNewTab: () => void
|
||||
/**
|
||||
* Callback when copy link is clicked
|
||||
*/
|
||||
onCopyLink: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu component for sidebar navigation items.
|
||||
* Displays navigation-appropriate options (open in new tab, copy link) in a popover at the right-click position.
|
||||
*/
|
||||
export function NavItemContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
menuRef,
|
||||
onClose,
|
||||
onOpenInNewTab,
|
||||
onCopyLink,
|
||||
}: NavItemContextMenuProps) {
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
}}
|
||||
/>
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onOpenInNewTab()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Open in new tab
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onCopyLink()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Copy link
|
||||
</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -150,7 +150,7 @@ export function ContextMenu({
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onClose}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/provide
|
||||
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
||||
import {
|
||||
HelpModal,
|
||||
NavItemContextMenu,
|
||||
SearchModal,
|
||||
SettingsModal,
|
||||
UsageIndicator,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
WorkspaceHeader,
|
||||
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
|
||||
import {
|
||||
useContextMenu,
|
||||
useFolderOperations,
|
||||
useSidebarResize,
|
||||
useWorkflowOperations,
|
||||
@@ -168,6 +170,46 @@ export function Sidebar() {
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
/** Context menu state for navigation items */
|
||||
const [activeNavItemHref, setActiveNavItemHref] = useState<string | null>(null)
|
||||
const {
|
||||
isOpen: isNavContextMenuOpen,
|
||||
position: navContextMenuPosition,
|
||||
menuRef: navMenuRef,
|
||||
handleContextMenu: handleNavContextMenuBase,
|
||||
closeMenu: closeNavContextMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
const handleNavItemContextMenu = useCallback(
|
||||
(e: React.MouseEvent, href: string) => {
|
||||
setActiveNavItemHref(href)
|
||||
handleNavContextMenuBase(e)
|
||||
},
|
||||
[handleNavContextMenuBase]
|
||||
)
|
||||
|
||||
const handleNavContextMenuClose = useCallback(() => {
|
||||
closeNavContextMenu()
|
||||
setActiveNavItemHref(null)
|
||||
}, [closeNavContextMenu])
|
||||
|
||||
const handleNavOpenInNewTab = useCallback(() => {
|
||||
if (activeNavItemHref) {
|
||||
window.open(activeNavItemHref, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}, [activeNavItemHref])
|
||||
|
||||
const handleNavCopyLink = useCallback(async () => {
|
||||
if (activeNavItemHref) {
|
||||
const fullUrl = `${window.location.origin}${activeNavItemHref}`
|
||||
try {
|
||||
await navigator.clipboard.writeText(fullUrl)
|
||||
} catch (error) {
|
||||
logger.error('Failed to copy link to clipboard', { error })
|
||||
}
|
||||
}
|
||||
}, [activeNavItemHref])
|
||||
|
||||
const { handleDuplicateWorkspace: duplicateWorkspace } = useDuplicateWorkspace({
|
||||
getWorkspaceId: () => workspaceId,
|
||||
})
|
||||
@@ -629,12 +671,23 @@ export function Sidebar() {
|
||||
href={item.href!}
|
||||
data-item-id={item.id}
|
||||
className={`${baseClasses} ${activeClasses}`}
|
||||
onContextMenu={(e) => handleNavItemContextMenu(e, item.href!)}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Nav Item Context Menu */}
|
||||
<NavItemContextMenu
|
||||
isOpen={isNavContextMenuOpen}
|
||||
position={navContextMenuPosition}
|
||||
menuRef={navMenuRef}
|
||||
onClose={handleNavContextMenuClose}
|
||||
onOpenInNewTab={handleNavOpenInNewTab}
|
||||
onCopyLink={handleNavCopyLink}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
category: 'tools',
|
||||
bgColor: '#5865F2',
|
||||
icon: DiscordIcon,
|
||||
docsLink: 'https://docs.sim.ai/tools/discord',
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
|
||||
@@ -12,7 +12,7 @@ export const FirefliesBlock: BlockConfig<FirefliesResponse> = {
|
||||
triggerAllowed: true,
|
||||
longDescription:
|
||||
'Integrate Fireflies.ai into the workflow. Manage meeting transcripts, add bot to live meetings, create soundbites, and more. Can also trigger workflows when transcriptions complete.',
|
||||
docsLink: 'https://docs.fireflies.ai',
|
||||
docsLink: 'https://docs.sim.ai/tools/fireflies',
|
||||
category: 'tools',
|
||||
icon: FirefliesIcon,
|
||||
bgColor: '#100730',
|
||||
|
||||
@@ -13,6 +13,7 @@ export const GenericWebhookBlock: BlockConfig = {
|
||||
category: 'triggers',
|
||||
icon: WebhookIcon,
|
||||
bgColor: '#10B981', // Green color for triggers
|
||||
docsLink: 'https://docs.sim.ai/triggers/webhook',
|
||||
triggerAllowed: true,
|
||||
bestPractices: `
|
||||
- You can test the webhook by sending a request to the webhook URL. E.g. depending on authorization: curl -X POST http://localhost:3000/api/webhooks/trigger/d8abcf0d-1ee5-4b77-bb07-b1e8142ea4e9 -H "Content-Type: application/json" -H "X-Sim-Secret: 1234" -d '{"message": "Test webhook trigger", "data": {"key": "v"}}'
|
||||
|
||||
@@ -13,6 +13,7 @@ export const GrainBlock: BlockConfig = {
|
||||
longDescription:
|
||||
'Integrate Grain into your workflow. Access meeting recordings, transcripts, highlights, and AI-generated summaries. Can also trigger workflows based on Grain webhook events.',
|
||||
category: 'tools',
|
||||
docsLink: 'https://docs.sim.ai/tools/grain',
|
||||
icon: GrainIcon,
|
||||
bgColor: '#F6FAF9',
|
||||
subBlocks: [
|
||||
|
||||
@@ -12,6 +12,7 @@ export const ImapBlock: BlockConfig = {
|
||||
bgColor: '#6366F1',
|
||||
icon: MailServerIcon,
|
||||
triggerAllowed: true,
|
||||
docsLink: 'https://docs.sim.ai/tools/imap',
|
||||
hideFromToolbar: false,
|
||||
subBlocks: [...getTrigger('imap_poller').subBlocks],
|
||||
tools: {
|
||||
|
||||
@@ -164,6 +164,7 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
|
||||
name: 'Router (Legacy)',
|
||||
description: 'Route workflow',
|
||||
authMode: AuthMode.ApiKey,
|
||||
docsLink: 'https://docs.sim.ai/blocks/router',
|
||||
longDescription:
|
||||
'This is a core workflow block. Intelligently direct workflow execution to different paths based on input analysis. Use natural language to instruct the router to route to certain blocks based on the input.',
|
||||
bestPractices: `
|
||||
@@ -283,6 +284,7 @@ export const RouterV2Block: BlockConfig<RouterV2Response> = {
|
||||
name: 'Router',
|
||||
description: 'Route workflow based on context',
|
||||
authMode: AuthMode.ApiKey,
|
||||
docsLink: 'https://docs.sim.ai/blocks/router',
|
||||
longDescription:
|
||||
'Intelligently route workflow execution to different paths based on context analysis. Define multiple routes with descriptions, and an LLM will determine which route to take based on the provided context.',
|
||||
bestPractices: `
|
||||
|
||||
@@ -12,6 +12,7 @@ export const RssBlock: BlockConfig = {
|
||||
bgColor: '#F97316',
|
||||
icon: RssIcon,
|
||||
triggerAllowed: true,
|
||||
docsLink: 'https://docs.sim.ai/triggers/rss',
|
||||
|
||||
subBlocks: [...getTrigger('rss_poller').subBlocks],
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const StartTriggerBlock: BlockConfig = {
|
||||
`,
|
||||
category: 'triggers',
|
||||
bgColor: '#34B5FF',
|
||||
docsLink: 'https://docs.sim.ai/triggers/start',
|
||||
icon: StartIcon,
|
||||
hideFromToolbar: false,
|
||||
subBlocks: [
|
||||
|
||||
@@ -10,6 +10,7 @@ export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Twilio into the workflow. Can send SMS messages.',
|
||||
category: 'tools',
|
||||
docsLink: 'https://docs.sim.ai/tools/twilio',
|
||||
bgColor: '#F22F46', // Twilio brand color
|
||||
icon: TwilioIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -12,6 +12,7 @@ export const TwilioVoiceBlock: BlockConfig<ToolResponse> = {
|
||||
longDescription:
|
||||
'Integrate Twilio Voice into the workflow. Make outbound calls and retrieve call recordings.',
|
||||
category: 'tools',
|
||||
docsLink: 'https://docs.sim.ai/tools/twilio_voice',
|
||||
bgColor: '#F22F46', // Twilio brand color
|
||||
icon: TwilioIcon,
|
||||
triggerAllowed: true,
|
||||
|
||||
Reference in New Issue
Block a user