mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
improvement(ui/ux): workflow, search modal (#729)
* improvement: workflow colors * fix: workflow rename styling * improvement: no API call on no name change workspace after edit * improvement: added workflow and workspace to search * improvement: folder path opened for current workflow on load * improvement: ui/ux workspace selector * improvement: search modal keyboard use
This commit is contained in:
@@ -135,43 +135,58 @@ interface TemplateCardProps {
|
||||
// Skeleton component for loading states
|
||||
export function TemplateCardSkeleton({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn('rounded-[14px] border bg-card shadow-xs', 'flex h-38', className)}>
|
||||
<div className={cn('rounded-[14px] border bg-card shadow-xs', 'flex h-[142px]', className)}>
|
||||
{/* Left side - Info skeleton */}
|
||||
<div className='flex min-w-0 flex-1 flex-col justify-between p-4'>
|
||||
{/* Top section skeleton */}
|
||||
<div className='space-y-3'>
|
||||
<div className='flex min-w-0 items-center gap-2.5'>
|
||||
{/* Icon skeleton */}
|
||||
<div className='h-5 w-5 flex-shrink-0 animate-pulse rounded bg-gray-200' />
|
||||
{/* Title skeleton */}
|
||||
<div className='h-4 w-24 animate-pulse rounded bg-gray-200' />
|
||||
<div className='space-y-2'>
|
||||
<div className='flex min-w-0 items-center justify-between gap-2.5'>
|
||||
<div className='flex min-w-0 items-center gap-2.5'>
|
||||
{/* Icon skeleton */}
|
||||
<div className='h-5 w-5 flex-shrink-0 animate-pulse rounded-md bg-gray-200' />
|
||||
{/* Title skeleton */}
|
||||
<div className='h-4 w-32 animate-pulse rounded bg-gray-200' />
|
||||
</div>
|
||||
|
||||
{/* Star and Use button skeleton */}
|
||||
<div className='flex flex-shrink-0 items-center gap-3'>
|
||||
<div className='h-4 w-4 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-6 w-10 animate-pulse rounded-md bg-gray-200' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description skeleton */}
|
||||
<div className='space-y-2'>
|
||||
<div className='space-y-1.5'>
|
||||
<div className='h-3 w-full animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-3/4 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-1/2 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-4/5 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-3/5 animate-pulse rounded bg-gray-200' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section skeleton */}
|
||||
<div className='flex min-w-0 items-center gap-1.5'>
|
||||
<div className='h-3 w-8 animate-pulse rounded bg-gray-200' />
|
||||
<div className='flex min-w-0 items-center gap-1.5 pt-1.5'>
|
||||
<div className='h-3 w-6 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-16 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-1 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-2 w-1 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-3 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-8 animate-pulse rounded bg-gray-200' />
|
||||
{/* Stars section - hidden on smaller screens */}
|
||||
<div className='hidden flex-shrink-0 items-center gap-1.5 sm:flex'>
|
||||
<div className='h-2 w-1 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-3 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-6 animate-pulse rounded bg-gray-200' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Blocks skeleton */}
|
||||
<div className='flex w-16 flex-col gap-1 rounded-r-[14px] border-border border-l bg-secondary p-2'>
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index} className='flex items-center gap-1.5'>
|
||||
<div className='h-3 w-3 animate-pulse rounded bg-gray-200' />
|
||||
<div className='h-3 w-12 animate-pulse rounded bg-gray-200' />
|
||||
</div>
|
||||
{/* Right side - Block Icons skeleton */}
|
||||
<div className='flex w-16 flex-col items-center justify-center gap-2 rounded-r-[14px] border-border border-l bg-secondary p-2'>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='animate-pulse rounded bg-gray-200'
|
||||
style={{ width: '30px', height: '30px' }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
|
||||
import { BookOpen, LibraryBig, ScrollText, Search, Shapes } from 'lucide-react'
|
||||
import { BookOpen, Building2, LibraryBig, ScrollText, Search, Shapes, Workflow } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Dialog, DialogOverlay, DialogPortal, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -15,7 +15,10 @@ interface SearchModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
templates?: TemplateData[]
|
||||
workflows?: WorkflowItem[]
|
||||
workspaces?: WorkspaceItem[]
|
||||
loading?: boolean
|
||||
isOnWorkflowPage?: boolean
|
||||
}
|
||||
|
||||
interface TemplateData {
|
||||
@@ -33,6 +36,20 @@ interface TemplateData {
|
||||
isStarred?: boolean
|
||||
}
|
||||
|
||||
interface WorkflowItem {
|
||||
id: string
|
||||
name: string
|
||||
href: string
|
||||
isCurrent?: boolean
|
||||
}
|
||||
|
||||
interface WorkspaceItem {
|
||||
id: string
|
||||
name: string
|
||||
href: string
|
||||
isCurrent?: boolean
|
||||
}
|
||||
|
||||
interface BlockItem {
|
||||
id: string
|
||||
name: string
|
||||
@@ -69,9 +86,13 @@ export function SearchModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
templates = [],
|
||||
workflows = [],
|
||||
workspaces = [],
|
||||
loading = false,
|
||||
isOnWorkflowPage = false,
|
||||
}: SearchModalProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const workspaceId = params.workspaceId as string
|
||||
@@ -115,8 +136,10 @@ export function SearchModal({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Get all available blocks
|
||||
// Get all available blocks - only when on workflow page
|
||||
const blocks = useMemo(() => {
|
||||
if (!isOnWorkflowPage) return []
|
||||
|
||||
const allBlocks = getAllBlocks()
|
||||
return allBlocks
|
||||
.filter(
|
||||
@@ -132,10 +155,12 @@ export function SearchModal({
|
||||
})
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [])
|
||||
}, [isOnWorkflowPage])
|
||||
|
||||
// Get all available tools
|
||||
// Get all available tools - only when on workflow page
|
||||
const tools = useMemo(() => {
|
||||
if (!isOnWorkflowPage) return []
|
||||
|
||||
const allBlocks = getAllBlocks()
|
||||
return allBlocks
|
||||
.filter((block) => block.category === 'tools')
|
||||
@@ -149,7 +174,7 @@ export function SearchModal({
|
||||
})
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [])
|
||||
}, [isOnWorkflowPage])
|
||||
|
||||
// Define pages
|
||||
const pages = useMemo(
|
||||
@@ -230,6 +255,18 @@ export function SearchModal({
|
||||
.slice(0, 8)
|
||||
}, [localTemplates, searchQuery])
|
||||
|
||||
const filteredWorkflows = useMemo(() => {
|
||||
if (!searchQuery.trim()) return workflows
|
||||
const query = searchQuery.toLowerCase()
|
||||
return workflows.filter((workflow) => workflow.name.toLowerCase().includes(query))
|
||||
}, [workflows, searchQuery])
|
||||
|
||||
const filteredWorkspaces = useMemo(() => {
|
||||
if (!searchQuery.trim()) return workspaces
|
||||
const query = searchQuery.toLowerCase()
|
||||
return workspaces.filter((workspace) => workspace.name.toLowerCase().includes(query))
|
||||
}, [workspaces, searchQuery])
|
||||
|
||||
const filteredPages = useMemo(() => {
|
||||
if (!searchQuery.trim()) return pages
|
||||
const query = searchQuery.toLowerCase()
|
||||
@@ -242,6 +279,42 @@ export function SearchModal({
|
||||
return docs.filter((doc) => doc.name.toLowerCase().includes(query))
|
||||
}, [docs, searchQuery])
|
||||
|
||||
// Create flattened list of navigatable items for keyboard navigation
|
||||
const navigatableItems = useMemo(() => {
|
||||
const items: Array<{
|
||||
type: 'workspace' | 'workflow' | 'page' | 'doc'
|
||||
data: any
|
||||
section: string
|
||||
}> = []
|
||||
|
||||
// Add workspaces
|
||||
filteredWorkspaces.forEach((workspace) => {
|
||||
items.push({ type: 'workspace', data: workspace, section: 'Workspaces' })
|
||||
})
|
||||
|
||||
// Add workflows
|
||||
filteredWorkflows.forEach((workflow) => {
|
||||
items.push({ type: 'workflow', data: workflow, section: 'Workflows' })
|
||||
})
|
||||
|
||||
// Add pages
|
||||
filteredPages.forEach((page) => {
|
||||
items.push({ type: 'page', data: page, section: 'Pages' })
|
||||
})
|
||||
|
||||
// Add docs
|
||||
filteredDocs.forEach((doc) => {
|
||||
items.push({ type: 'doc', data: doc, section: 'Docs' })
|
||||
})
|
||||
|
||||
return items
|
||||
}, [filteredWorkspaces, filteredWorkflows, filteredPages, filteredDocs])
|
||||
|
||||
// Reset selected index when items change or modal opens
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0)
|
||||
}, [navigatableItems, open])
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -292,6 +365,15 @@ export function SearchModal({
|
||||
[router, onOpenChange]
|
||||
)
|
||||
|
||||
// Handle workflow/workspace navigation (same as page navigation)
|
||||
const handleNavigationClick = useCallback(
|
||||
(href: string) => {
|
||||
router.push(href)
|
||||
onOpenChange(false)
|
||||
},
|
||||
[router, onOpenChange]
|
||||
)
|
||||
|
||||
// Handle docs navigation
|
||||
const handleDocsClick = useCallback(
|
||||
(href: string) => {
|
||||
@@ -360,6 +442,89 @@ export function SearchModal({
|
||||
[]
|
||||
)
|
||||
|
||||
// Handle item selection based on type
|
||||
const handleItemSelection = useCallback(
|
||||
(item: (typeof navigatableItems)[0]) => {
|
||||
switch (item.type) {
|
||||
case 'workspace':
|
||||
if (item.data.isCurrent) {
|
||||
onOpenChange(false)
|
||||
} else {
|
||||
handleNavigationClick(item.data.href)
|
||||
}
|
||||
break
|
||||
case 'workflow':
|
||||
if (item.data.isCurrent) {
|
||||
onOpenChange(false)
|
||||
} else {
|
||||
handleNavigationClick(item.data.href)
|
||||
}
|
||||
break
|
||||
case 'page':
|
||||
handlePageClick(item.data.href)
|
||||
break
|
||||
case 'doc':
|
||||
handleDocsClick(item.data.href)
|
||||
break
|
||||
}
|
||||
},
|
||||
[handleNavigationClick, handlePageClick, handleDocsClick, onOpenChange]
|
||||
)
|
||||
|
||||
// Handle keyboard navigation
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => Math.min(prev + 1, navigatableItems.length - 1))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => Math.max(prev - 1, 0))
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
if (navigatableItems.length > 0 && selectedIndex < navigatableItems.length) {
|
||||
const selectedItem = navigatableItems[selectedIndex]
|
||||
handleItemSelection(selectedItem)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
onOpenChange(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [open, selectedIndex, navigatableItems, onOpenChange, handleItemSelection])
|
||||
|
||||
// Helper function to check if an item is selected
|
||||
const isItemSelected = useCallback(
|
||||
(item: any, itemType: string) => {
|
||||
if (navigatableItems.length === 0 || selectedIndex >= navigatableItems.length) return false
|
||||
const selectedItem = navigatableItems[selectedIndex]
|
||||
return selectedItem.type === itemType && selectedItem.data.id === item.id
|
||||
},
|
||||
[navigatableItems, selectedIndex]
|
||||
)
|
||||
|
||||
// Scroll selected item into view
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0 && navigatableItems.length > 0) {
|
||||
const selectedItem = navigatableItems[selectedIndex]
|
||||
const itemElement = document.querySelector(
|
||||
`[data-search-item="${selectedItem.type}-${selectedItem.data.id}"]`
|
||||
)
|
||||
if (itemElement) {
|
||||
itemElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
}, [selectedIndex, navigatableItems])
|
||||
|
||||
// Render skeleton cards for loading state
|
||||
const renderSkeletonCards = () => {
|
||||
return Array.from({ length: 8 }).map((_, index) => (
|
||||
@@ -560,6 +725,76 @@ export function SearchModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Workspaces Section */}
|
||||
{filteredWorkspaces.length > 0 && (
|
||||
<div>
|
||||
<h3 className='mb-3 ml-6 font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
|
||||
Workspaces
|
||||
</h3>
|
||||
<div className='space-y-1 px-6'>
|
||||
{filteredWorkspaces.map((workspace) => (
|
||||
<button
|
||||
key={workspace.id}
|
||||
onClick={() =>
|
||||
workspace.isCurrent
|
||||
? onOpenChange(false)
|
||||
: handleNavigationClick(workspace.href)
|
||||
}
|
||||
data-search-item={`workspace-${workspace.id}`}
|
||||
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors focus:outline-none ${
|
||||
isItemSelected(workspace, 'workspace')
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent/60 focus:bg-accent/60'
|
||||
}`}
|
||||
>
|
||||
<div className='flex h-5 w-5 items-center justify-center'>
|
||||
<Building2 className='h-4 w-4 text-muted-foreground' />
|
||||
</div>
|
||||
<span className='flex-1 text-left font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
|
||||
{workspace.name}
|
||||
{workspace.isCurrent && ' (current)'}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Workflows Section */}
|
||||
{filteredWorkflows.length > 0 && (
|
||||
<div>
|
||||
<h3 className='mb-3 ml-6 font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
|
||||
Workflows
|
||||
</h3>
|
||||
<div className='space-y-1 px-6'>
|
||||
{filteredWorkflows.map((workflow) => (
|
||||
<button
|
||||
key={workflow.id}
|
||||
onClick={() =>
|
||||
workflow.isCurrent
|
||||
? onOpenChange(false)
|
||||
: handleNavigationClick(workflow.href)
|
||||
}
|
||||
data-search-item={`workflow-${workflow.id}`}
|
||||
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors focus:outline-none ${
|
||||
isItemSelected(workflow, 'workflow')
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent/60 focus:bg-accent/60'
|
||||
}`}
|
||||
>
|
||||
<div className='flex h-5 w-5 items-center justify-center'>
|
||||
<Workflow className='h-4 w-4 text-muted-foreground' />
|
||||
</div>
|
||||
<span className='flex-1 text-left font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
|
||||
{workflow.name}
|
||||
{workflow.isCurrent && ' (current)'}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pages Section */}
|
||||
{filteredPages.length > 0 && (
|
||||
<div>
|
||||
@@ -571,7 +806,12 @@ export function SearchModal({
|
||||
<button
|
||||
key={page.id}
|
||||
onClick={() => handlePageClick(page.href)}
|
||||
className='flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-accent/60 focus:bg-accent/60 focus:outline-none'
|
||||
data-search-item={`page-${page.id}`}
|
||||
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors focus:outline-none ${
|
||||
isItemSelected(page, 'page')
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent/60 focus:bg-accent/60'
|
||||
}`}
|
||||
>
|
||||
<div className='flex h-5 w-5 items-center justify-center'>
|
||||
<page.icon className='h-4 w-4 text-muted-foreground' />
|
||||
@@ -605,7 +845,12 @@ export function SearchModal({
|
||||
<button
|
||||
key={doc.id}
|
||||
onClick={() => handleDocsClick(doc.href)}
|
||||
className='flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-accent/60 focus:bg-accent/60 focus:outline-none'
|
||||
data-search-item={`doc-${doc.id}`}
|
||||
className={`flex h-10 w-full items-center gap-3 rounded-lg px-3 py-2 transition-colors focus:outline-none ${
|
||||
isItemSelected(doc, 'doc')
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent/60 focus:bg-accent/60'
|
||||
}`}
|
||||
>
|
||||
<div className='flex h-5 w-5 items-center justify-center'>
|
||||
<doc.icon className='h-4 w-4 text-muted-foreground' />
|
||||
@@ -622,11 +867,13 @@ export function SearchModal({
|
||||
{/* Empty state */}
|
||||
{searchQuery &&
|
||||
!loading &&
|
||||
filteredWorkflows.length === 0 &&
|
||||
filteredWorkspaces.length === 0 &&
|
||||
filteredPages.length === 0 &&
|
||||
filteredDocs.length === 0 &&
|
||||
filteredBlocks.length === 0 &&
|
||||
filteredTools.length === 0 &&
|
||||
filteredTemplates.length === 0 &&
|
||||
filteredPages.length === 0 &&
|
||||
filteredDocs.length === 0 && (
|
||||
filteredTemplates.length === 0 && (
|
||||
<div className='ml-6 py-12 text-center'>
|
||||
<p className='text-muted-foreground'>No results found for "{searchQuery}"</p>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { type FolderTreeNode, useFolderStore } from '@/stores/folders/store'
|
||||
@@ -299,16 +298,20 @@ export function FolderItem({
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<Input
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
className='h-6 flex-1 border-0 bg-transparent p-0 text-muted-foreground text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
className='flex-1 border-0 bg-transparent p-0 text-muted-foreground text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
maxLength={50}
|
||||
disabled={isRenaming}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent folder toggle when clicking input
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
/>
|
||||
) : (
|
||||
<span className='flex-1 select-none truncate text-muted-foreground'>{folder.name}</span>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useFolderStore, useIsWorkflowSelected } from '@/stores/folders/store'
|
||||
@@ -220,16 +219,22 @@ export function WorkflowItem({
|
||||
style={{ backgroundColor: workflow.color }}
|
||||
/>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
className='h-6 flex-1 border-0 bg-transparent p-0 text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
className={`flex-1 border-0 bg-transparent p-0 font-medium text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 ${
|
||||
active && !isDragOver ? 'text-foreground' : 'text-muted-foreground'
|
||||
}`}
|
||||
maxLength={100}
|
||||
disabled={isRenaming}
|
||||
onClick={(e) => e.preventDefault()} // Prevent navigation when clicking input
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
/>
|
||||
) : (
|
||||
<span className='flex-1 select-none truncate'>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useParams, usePathname } from 'next/navigation'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
@@ -354,6 +354,7 @@ export function FolderTree({
|
||||
const pathname = usePathname()
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const workflowId = params.workflowId as string
|
||||
const {
|
||||
getFolderTree,
|
||||
expandedFolders,
|
||||
@@ -361,9 +362,33 @@ export function FolderTree({
|
||||
isLoading: foldersLoading,
|
||||
clearSelection,
|
||||
updateFolderAPI,
|
||||
getFolderPath,
|
||||
setExpanded,
|
||||
} = useFolderStore()
|
||||
const { updateWorkflow } = useWorkflowRegistry()
|
||||
|
||||
// Memoize the active workflow's folder ID to avoid unnecessary re-runs
|
||||
const activeWorkflowFolderId = useMemo(() => {
|
||||
if (!workflowId || isLoading || foldersLoading) return null
|
||||
const activeWorkflow = regularWorkflows.find((workflow) => workflow.id === workflowId)
|
||||
return activeWorkflow?.folderId || null
|
||||
}, [workflowId, regularWorkflows, isLoading, foldersLoading])
|
||||
|
||||
// Auto-expand folders when a workflow is active
|
||||
useEffect(() => {
|
||||
if (!activeWorkflowFolderId) return
|
||||
|
||||
// Get the folder path from root to the workflow's folder
|
||||
const folderPath = getFolderPath(activeWorkflowFolderId)
|
||||
|
||||
// Expand all folders in the path (only if not already expanded)
|
||||
folderPath.forEach((folder) => {
|
||||
if (!expandedFolders.has(folder.id)) {
|
||||
setExpanded(folder.id, true)
|
||||
}
|
||||
})
|
||||
}, [activeWorkflowFolderId, getFolderPath, setExpanded])
|
||||
|
||||
// Clean up any existing folders with 3+ levels of nesting
|
||||
const cleanupDeepNesting = useCallback(async () => {
|
||||
const { getFolderTree, updateFolderAPI } = useFolderStore.getState()
|
||||
|
||||
@@ -104,8 +104,9 @@ export const WorkspaceHeader = React.memo<WorkspaceHeaderProps>(
|
||||
case 'save': {
|
||||
// Exit edit mode immediately, save in background
|
||||
setIsEditingName(false)
|
||||
if (activeWorkspace && editingName.trim() !== '') {
|
||||
updateWorkspaceName(activeWorkspace.id, editingName.trim()).catch((error) => {
|
||||
const trimmedName = editingName.trim()
|
||||
if (activeWorkspace && trimmedName !== '' && trimmedName !== activeWorkspace.name) {
|
||||
updateWorkspaceName(activeWorkspace.id, trimmedName).catch((error) => {
|
||||
logger.error('Failed to update workspace name:', error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ interface WorkspaceSelectorProps {
|
||||
onLeaveWorkspace: (workspace: Workspace) => Promise<void>
|
||||
isDeleting: boolean
|
||||
isLeaving: boolean
|
||||
isCreating: boolean
|
||||
}
|
||||
|
||||
export function WorkspaceSelector({
|
||||
@@ -59,6 +60,7 @@ export function WorkspaceSelector({
|
||||
onLeaveWorkspace,
|
||||
isDeleting,
|
||||
isLeaving,
|
||||
isCreating,
|
||||
}: WorkspaceSelectorProps) {
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
@@ -256,7 +258,7 @@ export function WorkspaceSelector({
|
||||
onClick={userPermissions.canAdmin ? () => setShowInviteMembers(true) : undefined}
|
||||
disabled={!userPermissions.canAdmin}
|
||||
className={cn(
|
||||
'h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs hover:bg-secondary hover:text-muted-foreground',
|
||||
'h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs transition-colors hover:bg-muted-foreground/10 hover:text-muted-foreground',
|
||||
!userPermissions.canAdmin && 'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
>
|
||||
@@ -269,7 +271,11 @@ export function WorkspaceSelector({
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
onClick={onCreateWorkspace}
|
||||
className='h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs hover:bg-secondary hover:text-muted-foreground'
|
||||
disabled={isCreating}
|
||||
className={cn(
|
||||
'h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs transition-colors hover:bg-muted-foreground/10 hover:text-muted-foreground',
|
||||
isCreating && 'cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Plus className='h-3 w-3' />
|
||||
<span>Create</span>
|
||||
|
||||
@@ -86,6 +86,8 @@ export function Sidebar() {
|
||||
|
||||
// Add state to prevent multiple simultaneous workflow creations
|
||||
const [isCreatingWorkflow, setIsCreatingWorkflow] = useState(false)
|
||||
// Add state to prevent multiple simultaneous workspace creations
|
||||
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false)
|
||||
// Add sidebar collapsed state
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
|
||||
const params = useParams()
|
||||
@@ -276,7 +278,13 @@ export function Sidebar() {
|
||||
* Handle create workspace
|
||||
*/
|
||||
const handleCreateWorkspace = useCallback(async () => {
|
||||
if (isCreatingWorkspace) {
|
||||
logger.info('Workspace creation already in progress, ignoring request')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCreatingWorkspace(true)
|
||||
logger.info('Creating new workspace')
|
||||
|
||||
const response = await fetch('/api/workspaces', {
|
||||
@@ -306,8 +314,10 @@ export function Sidebar() {
|
||||
await switchWorkspace(newWorkspace)
|
||||
} catch (error) {
|
||||
logger.error('Error creating workspace:', error)
|
||||
} finally {
|
||||
setIsCreatingWorkspace(false)
|
||||
}
|
||||
}, [refreshWorkspaceList, switchWorkspace])
|
||||
}, [refreshWorkspaceList, switchWorkspace, isCreatingWorkspace])
|
||||
|
||||
/**
|
||||
* Confirm delete workspace
|
||||
@@ -570,6 +580,29 @@ export function Sidebar() {
|
||||
return { regularWorkflows: regular, tempWorkflows: temp }
|
||||
}, [workflows, isLoading, workspaceId])
|
||||
|
||||
// Prepare workflows for search modal
|
||||
const searchWorkflows = useMemo(() => {
|
||||
if (isLoading) return []
|
||||
|
||||
const allWorkflows = [...regularWorkflows, ...tempWorkflows]
|
||||
return allWorkflows.map((workflow) => ({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
href: `/workspace/${workspaceId}/w/${workflow.id}`,
|
||||
isCurrent: workflow.id === workflowId,
|
||||
}))
|
||||
}, [regularWorkflows, tempWorkflows, workspaceId, workflowId, isLoading])
|
||||
|
||||
// Prepare workspaces for search modal (include all workspaces)
|
||||
const searchWorkspaces = useMemo(() => {
|
||||
return workspaces.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
href: `/workspace/${workspace.id}/w`,
|
||||
isCurrent: workspace.id === workspaceId,
|
||||
}))
|
||||
}, [workspaces, workspaceId])
|
||||
|
||||
// Create workflow handler
|
||||
const handleCreateWorkflow = async (folderId?: string): Promise<string> => {
|
||||
if (isCreatingWorkflow) {
|
||||
@@ -752,6 +785,7 @@ export function Sidebar() {
|
||||
onLeaveWorkspace={handleLeaveWorkspace}
|
||||
isDeleting={isDeleting}
|
||||
isLeaving={isLeaving}
|
||||
isCreating={isCreatingWorkspace}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -783,7 +817,7 @@ export function Sidebar() {
|
||||
}`}
|
||||
>
|
||||
<div className='px-2'>
|
||||
<ScrollArea ref={workflowScrollAreaRef} className='h-[212px]' hideScrollbar={true}>
|
||||
<ScrollArea ref={workflowScrollAreaRef} className='h-[210px]' hideScrollbar={true}>
|
||||
<FolderTree
|
||||
regularWorkflows={regularWorkflows}
|
||||
marketplaceWorkflows={tempWorkflows}
|
||||
@@ -838,7 +872,15 @@ export function Sidebar() {
|
||||
<SettingsModal open={showSettings} onOpenChange={setShowSettings} />
|
||||
<HelpModal open={showHelp} onOpenChange={setShowHelp} />
|
||||
<InviteModal open={showInviteMembers} onOpenChange={setShowInviteMembers} />
|
||||
<SearchModal open={showSearchModal} onOpenChange={setShowSearchModal} templates={templates} />
|
||||
<SearchModal
|
||||
open={showSearchModal}
|
||||
onOpenChange={setShowSearchModal}
|
||||
templates={templates}
|
||||
workflows={searchWorkflows}
|
||||
workspaces={searchWorkspaces}
|
||||
loading={isTemplatesLoading}
|
||||
isOnWorkflowPage={isOnWorkflowPage}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export const workflowBlocks = pgTable(
|
||||
data: jsonb('data').default('{}'),
|
||||
|
||||
parentId: text('parent_id'),
|
||||
extent: text('extent'), // 'parent' or null
|
||||
extent: text('extent'), // 'parent' or null or 'subflow'
|
||||
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
|
||||
@@ -1,93 +1,74 @@
|
||||
// Available workflow colors
|
||||
export const WORKFLOW_COLORS = [
|
||||
// Original colors
|
||||
'#3972F6', // Blue
|
||||
'#F639DD', // Pink/Magenta
|
||||
'#F6B539', // Orange/Yellow
|
||||
'#8139F6', // Purple
|
||||
'#39B54A', // Green
|
||||
'#39B5AB', // Teal
|
||||
'#F66839', // Red/Orange
|
||||
// Blues - vibrant blue tones
|
||||
'#3972F6', // Blue (original)
|
||||
'#2E5BF5', // Deeper Blue
|
||||
'#1E4BF4', // Royal Blue
|
||||
'#0D3BF3', // Deep Royal Blue
|
||||
|
||||
// Additional vibrant blues
|
||||
'#2E5BFF', // Bright Blue
|
||||
'#4A90FF', // Sky Blue
|
||||
'#1E40AF', // Deep Blue
|
||||
'#0EA5E9', // Cyan Blue
|
||||
'#3B82F6', // Royal Blue
|
||||
'#6366F1', // Indigo
|
||||
'#1D4ED8', // Electric Blue
|
||||
// Pinks/Magentas - vibrant pink and magenta tones
|
||||
'#F639DD', // Pink/Magenta (original)
|
||||
'#F529CF', // Deep Magenta
|
||||
'#F749E7', // Light Magenta
|
||||
'#F419C1', // Hot Pink
|
||||
|
||||
// Additional vibrant purples
|
||||
'#A855F7', // Bright Purple
|
||||
'#C084FC', // Light Purple
|
||||
'#7C3AED', // Deep Purple
|
||||
'#9333EA', // Violet
|
||||
'#8B5CF6', // Medium Purple
|
||||
'#6D28D9', // Dark Purple
|
||||
'#5B21B6', // Deep Violet
|
||||
// Oranges/Yellows - vibrant orange and yellow tones
|
||||
'#F6B539', // Orange/Yellow (original)
|
||||
'#F5A529', // Deep Orange
|
||||
'#F49519', // Burnt Orange
|
||||
'#F38509', // Deep Burnt Orange
|
||||
|
||||
// Additional vibrant pinks/magentas
|
||||
'#EC4899', // Hot Pink
|
||||
'#F97316', // Pink Orange
|
||||
'#E11D48', // Rose
|
||||
'#BE185D', // Deep Pink
|
||||
'#DB2777', // Pink Red
|
||||
'#F472B6', // Light Pink
|
||||
'#F59E0B', // Amber Pink
|
||||
// Purples - vibrant purple tones
|
||||
'#8139F6', // Purple (original)
|
||||
'#7129F5', // Deep Purple
|
||||
'#6119F4', // Royal Purple
|
||||
'#5109F3', // Deep Royal Purple
|
||||
|
||||
// Additional vibrant greens
|
||||
'#10B981', // Emerald
|
||||
'#059669', // Green Teal
|
||||
'#16A34A', // Forest Green
|
||||
'#22C55E', // Lime Green
|
||||
'#84CC16', // Yellow Green
|
||||
'#65A30D', // Olive Green
|
||||
'#15803D', // Dark Green
|
||||
// Greens - vibrant green tones
|
||||
'#39B54A', // Green (original)
|
||||
'#29A53A', // Deep Green
|
||||
'#19952A', // Forest Green
|
||||
'#09851A', // Deep Forest Green
|
||||
|
||||
// Additional vibrant teals/cyans
|
||||
'#06B6D4', // Cyan
|
||||
'#0891B2', // Dark Cyan
|
||||
'#0E7490', // Teal Blue
|
||||
'#14B8A6', // Turquoise
|
||||
'#0D9488', // Dark Teal
|
||||
'#047857', // Sea Green
|
||||
'#059669', // Mint Green
|
||||
// Teals/Cyans - vibrant teal and cyan tones
|
||||
'#39B5AB', // Teal (original)
|
||||
'#29A59B', // Deep Teal
|
||||
'#19958B', // Dark Teal
|
||||
'#09857B', // Deep Dark Teal
|
||||
|
||||
// Additional vibrant oranges/reds
|
||||
'#EA580C', // Bright Orange
|
||||
'#DC2626', // Red
|
||||
'#B91C1C', // Dark Red
|
||||
'#EF4444', // Light Red
|
||||
'#F97316', // Orange
|
||||
'#FB923C', // Light Orange
|
||||
'#FDBA74', // Peach
|
||||
// Reds/Red-Oranges - vibrant red and red-orange tones
|
||||
'#F66839', // Red/Orange (original)
|
||||
'#F55829', // Deep Red-Orange
|
||||
'#F44819', // Burnt Red
|
||||
'#F33809', // Deep Burnt Red
|
||||
|
||||
// Additional vibrant yellows/golds
|
||||
'#FBBF24', // Gold
|
||||
'#F59E0B', // Amber
|
||||
'#D97706', // Dark Amber
|
||||
'#92400E', // Bronze
|
||||
'#EAB308', // Yellow
|
||||
'#CA8A04', // Dark Yellow
|
||||
'#A16207', // Mustard
|
||||
// Additional vibrant colors for variety
|
||||
// Corals - warm coral tones
|
||||
'#F6397A', // Coral
|
||||
'#F5296A', // Deep Coral
|
||||
'#F7498A', // Light Coral
|
||||
|
||||
// Additional unique vibrant colors
|
||||
'#FF6B6B', // Coral
|
||||
'#4ECDC4', // Mint
|
||||
'#45B7D1', // Light Blue
|
||||
'#96CEB4', // Sage
|
||||
'#FFEAA7', // Cream
|
||||
'#DDA0DD', // Plum
|
||||
'#98D8C8', // Seafoam
|
||||
'#F7DC6F', // Banana
|
||||
'#BB8FCE', // Lavender
|
||||
'#85C1E9', // Baby Blue
|
||||
'#F8C471', // Peach
|
||||
'#82E0AA', // Light Green
|
||||
'#F1948A', // Salmon
|
||||
'#D7BDE2', // Lilac
|
||||
'#D7BDE2', // Lilac
|
||||
// Crimsons - deep red tones
|
||||
'#DC143C', // Crimson
|
||||
'#CC042C', // Deep Crimson
|
||||
'#EC243C', // Light Crimson
|
||||
'#BC003C', // Dark Crimson
|
||||
'#FC343C', // Bright Crimson
|
||||
|
||||
// Mint - fresh green tones
|
||||
'#00FF7F', // Mint Green
|
||||
'#00EF6F', // Deep Mint
|
||||
'#00DF5F', // Dark Mint
|
||||
|
||||
// Slate - blue-gray tones
|
||||
'#6A5ACD', // Slate Blue
|
||||
'#5A4ABD', // Deep Slate
|
||||
'#4A3AAD', // Dark Slate
|
||||
|
||||
// Amber - warm orange-yellow tones
|
||||
'#FFBF00', // Amber
|
||||
'#EFAF00', // Deep Amber
|
||||
'#DF9F00', // Dark Amber
|
||||
]
|
||||
|
||||
// Random adjectives and nouns for generating creative workflow names
|
||||
|
||||
Reference in New Issue
Block a user