mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(ux): command + enter to run workflow
This commit is contained in:
@@ -44,6 +44,7 @@ import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { getKeyboardShortcutText, useKeyboardShortcuts } from '../../hooks/use-keyboard-shortcuts'
|
||||
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
|
||||
import { DeploymentControls } from './components/deployment-controls/deployment-controls'
|
||||
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
|
||||
@@ -108,6 +109,20 @@ export function ControlBar() {
|
||||
const [isCancelling, setIsCancelling] = useState(false)
|
||||
const cancelFlagRef = useRef(false)
|
||||
|
||||
// Register keyboard shortcut for running workflow
|
||||
useKeyboardShortcuts(
|
||||
() => {
|
||||
if (!isExecuting && !isMultiRunning && !isCancelling) {
|
||||
if (isDebugModeEnabled) {
|
||||
handleRunWorkflow()
|
||||
} else {
|
||||
handleMultipleRuns()
|
||||
}
|
||||
}
|
||||
},
|
||||
isExecuting || isMultiRunning || isCancelling
|
||||
)
|
||||
|
||||
// Get notifications for current workflow
|
||||
const workflowNotifications = activeWorkflowId
|
||||
? getWorkflowNotifications(activeWorkflowId)
|
||||
@@ -820,44 +835,58 @@ export function ControlBar() {
|
||||
|
||||
<div className="flex ml-1">
|
||||
{/* Main Run/Debug Button */}
|
||||
<Button
|
||||
className={cn(
|
||||
'gap-2 font-medium',
|
||||
'bg-[#802FFF] hover:bg-[#7028E6]',
|
||||
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
|
||||
'text-white transition-all duration-200',
|
||||
(isExecuting || isMultiRunning) &&
|
||||
!isCancelling &&
|
||||
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
|
||||
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none',
|
||||
isDebugModeEnabled || isMultiRunning
|
||||
? 'rounded py-2 px-4 h-10'
|
||||
: 'rounded-r-none border-r border-r-[#6420cc] py-2 px-4 h-10'
|
||||
)}
|
||||
onClick={isDebugModeEnabled ? handleRunWorkflow : handleMultipleRuns}
|
||||
disabled={isExecuting || isMultiRunning || isCancelling}
|
||||
>
|
||||
{isCancelling ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
|
||||
) : isDebugModeEnabled ? (
|
||||
<Bug className={cn('h-3.5 w-3.5 mr-1.5', 'fill-current stroke-current')} />
|
||||
) : (
|
||||
<Play className={cn('h-3.5 w-3.5', 'fill-current stroke-current')} />
|
||||
)}
|
||||
{isCancelling
|
||||
? 'Cancelling...'
|
||||
: isMultiRunning
|
||||
? `Running (${completedRuns}/${runCount})`
|
||||
: isExecuting
|
||||
? isDebugging
|
||||
? 'Debugging'
|
||||
: 'Running'
|
||||
: isDebugModeEnabled
|
||||
? 'Debug'
|
||||
: runCount === 1
|
||||
? 'Run'
|
||||
: `Run (${runCount})`}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={cn(
|
||||
'gap-2 font-medium',
|
||||
'bg-[#802FFF] hover:bg-[#7028E6]',
|
||||
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
|
||||
'text-white transition-all duration-200',
|
||||
(isExecuting || isMultiRunning) &&
|
||||
!isCancelling &&
|
||||
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
|
||||
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none',
|
||||
isDebugModeEnabled || isMultiRunning
|
||||
? 'rounded py-2 px-4 h-10'
|
||||
: 'rounded-r-none border-r border-r-[#6420cc] py-2 px-4 h-10'
|
||||
)}
|
||||
onClick={isDebugModeEnabled ? handleRunWorkflow : handleMultipleRuns}
|
||||
disabled={isExecuting || isMultiRunning || isCancelling}
|
||||
>
|
||||
{isCancelling ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
|
||||
) : isDebugModeEnabled ? (
|
||||
<Bug className={cn('h-3.5 w-3.5 mr-1.5', 'fill-current stroke-current')} />
|
||||
) : (
|
||||
<Play className={cn('h-3.5 w-3.5', 'fill-current stroke-current')} />
|
||||
)}
|
||||
{isCancelling
|
||||
? 'Cancelling...'
|
||||
: isMultiRunning
|
||||
? `Running (${completedRuns}/${runCount})`
|
||||
: isExecuting
|
||||
? isDebugging
|
||||
? 'Debugging'
|
||||
: 'Running'
|
||||
: isDebugModeEnabled
|
||||
? 'Debug'
|
||||
: runCount === 1
|
||||
? 'Run'
|
||||
: `Run (${runCount})`}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isDebugModeEnabled
|
||||
? 'Debug Workflow'
|
||||
: runCount === 1
|
||||
? 'Run Workflow'
|
||||
: `Run Workflow ${runCount} times`}
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
{getKeyboardShortcutText('Enter', true)}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Dropdown Trigger - Only show when not in debug mode and not multi-running */}
|
||||
{!isDebugModeEnabled && !isMultiRunning && (
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Detect if the current platform is Mac
|
||||
*/
|
||||
export function isMacPlatform() {
|
||||
if (typeof navigator === 'undefined') return false
|
||||
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted keyboard shortcut string for display
|
||||
* @param key The key part of the shortcut (e.g., "Enter")
|
||||
* @param requiresCmd Whether the shortcut requires Cmd/Ctrl
|
||||
* @param requiresShift Whether the shortcut requires Shift
|
||||
* @param requiresAlt Whether the shortcut requires Alt/Option
|
||||
*/
|
||||
export function getKeyboardShortcutText(
|
||||
key: string,
|
||||
requiresCmd = false,
|
||||
requiresShift = false,
|
||||
requiresAlt = false
|
||||
) {
|
||||
const isMac = isMacPlatform()
|
||||
const cmdKey = isMac ? '⌘' : 'Ctrl'
|
||||
const altKey = isMac ? '⌥' : 'Alt'
|
||||
const shiftKey = '⇧'
|
||||
|
||||
const parts: string[] = []
|
||||
if (requiresCmd) parts.push(cmdKey)
|
||||
if (requiresShift) parts.push(shiftKey)
|
||||
if (requiresAlt) parts.push(altKey)
|
||||
parts.push(key)
|
||||
|
||||
return parts.join('+')
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage keyboard shortcuts
|
||||
* @param onRunWorkflow - Function to run when Cmd/Ctrl+Enter is pressed
|
||||
* @param isDisabled - Whether shortcuts should be disabled
|
||||
*/
|
||||
export function useKeyboardShortcuts(onRunWorkflow: () => void, isDisabled = false) {
|
||||
// Memoize the platform detection
|
||||
const isMac = useMemo(() => isMacPlatform(), [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Run workflow with Cmd+Enter (Mac) or Ctrl+Enter (Windows/Linux)
|
||||
if (event.key === 'Enter' && ((isMac && event.metaKey) || (!isMac && event.ctrlKey))) {
|
||||
// Don't trigger if user is typing in an input, textarea, or contenteditable element
|
||||
const activeElement = document.activeElement
|
||||
const isEditableElement =
|
||||
activeElement instanceof HTMLInputElement ||
|
||||
activeElement instanceof HTMLTextAreaElement ||
|
||||
activeElement?.hasAttribute('contenteditable')
|
||||
|
||||
if (!isEditableElement && !isDisabled) {
|
||||
event.preventDefault()
|
||||
onRunWorkflow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [onRunWorkflow, isDisabled, isMac])
|
||||
}
|
||||
Reference in New Issue
Block a user