1- import { memo , useCallback , useState } from 'react'
1+ import { type ComponentType , memo , useCallback , useState } from 'react'
22import {
33 Button ,
4+ Chip ,
45 cn ,
56 DropdownMenu ,
67 DropdownMenuContent ,
@@ -66,7 +67,7 @@ const INLINE_ICON_SIZE = 'size-[16px] shrink-0'
6667type ActionId = 'run' | 'enabled' | 'lock' | 'duplicate' | 'remove' | 'delete' | 'color'
6768
6869const INLINE_ACTION_WIDTH_STYLES : Record < ActionId , string > = {
69- run : '[--inline-action-width:64px ]' ,
70+ run : '[--inline-action-width:90px ]' ,
7071 enabled : '[--inline-action-width:86px]' ,
7172 lock : '[--inline-action-width:82px]' ,
7273 duplicate : '[--inline-action-width:100px]' ,
@@ -75,9 +76,21 @@ const INLINE_ACTION_WIDTH_STYLES: Record<ActionId, string> = {
7576 color : '[--inline-action-width:72px]' ,
7677}
7778
78- function InlineActionLabel ( { children } : { children : string } ) {
79+ function InlineActionLabel ( {
80+ children,
81+ persistent = false ,
82+ } : {
83+ children : string
84+ persistent ?: boolean
85+ } ) {
7986 return (
80- < span className = '-translate-x-1 ml-1.5 shrink-0 whitespace-nowrap font-medium text-small leading-none opacity-0 transition-[opacity,transform] duration-100 [transition-timing-function:cubic-bezier(0.2,0,0,1)] group-focus-within/inline-action:translate-x-0 group-focus-within/inline-action:opacity-100 group-hover/inline-action:translate-x-0 group-hover/inline-action:opacity-100 motion-reduce:transition-none' >
87+ < span
88+ className = { cn (
89+ 'ml-1.5 shrink-0 whitespace-nowrap font-medium text-small leading-none' ,
90+ ! persistent &&
91+ '-translate-x-1 opacity-0 transition-[opacity,transform] duration-100 [transition-timing-function:cubic-bezier(0.2,0,0,1)] group-focus-within/inline-action:translate-x-0 group-focus-within/inline-action:opacity-100 group-hover/inline-action:translate-x-0 group-hover/inline-action:opacity-100 motion-reduce:transition-none'
92+ ) }
93+ >
8194 { children }
8295 </ span >
8396 )
@@ -113,6 +126,36 @@ function RunningActionIcon({ inline = false }: { inline?: boolean }) {
113126 )
114127}
115128
129+ interface InlineBlockStatusProps {
130+ icon : ComponentType < { className ?: string } >
131+ label : string
132+ disabled : boolean
133+ onClick : ( ) => void
134+ }
135+
136+ function InlineBlockStatus ( { icon : Icon , label, disabled, onClick } : InlineBlockStatusProps ) {
137+ return (
138+ < Tooltip . Root preferAbove >
139+ < Tooltip . Trigger asChild >
140+ < span className = 'inline-flex' >
141+ < Chip
142+ variant = 'border'
143+ leftIcon = { Icon }
144+ aria-label = { label }
145+ className = 'size-[30px] justify-center p-0'
146+ disabled = { disabled }
147+ onClick = { ( event ) => {
148+ event . stopPropagation ( )
149+ onClick ( )
150+ } }
151+ />
152+ </ span >
153+ </ Tooltip . Trigger >
154+ < Tooltip . Content side = 'top' > { label } </ Tooltip . Content >
155+ </ Tooltip . Root >
156+ )
157+ }
158+
116159/**
117160 * Props for the ActionBar component
118161 */
@@ -125,6 +168,8 @@ interface ActionBarProps {
125168 disabled ?: boolean
126169 /** Places the actions inside the workflow card's border swell. */
127170 variant ?: 'floating' | 'swell' | 'inline'
171+ /** Limits an inline action bar to the block run control or overflow menu. */
172+ inlineActions ?: 'all' | 'run' | 'menu'
128173 /** Whether this block is currently executing. */
129174 isRunning ?: boolean
130175 /** Whether any block in the current workflow is executing. */
@@ -149,6 +194,7 @@ export const ActionBar = memo(
149194 blockType,
150195 disabled = false ,
151196 variant = 'floating' ,
197+ inlineActions = 'all' ,
152198 isRunning = false ,
153199 isWorkflowRunning = false ,
154200 noteColor = DEFAULT_NOTE_COLOR ,
@@ -217,6 +263,8 @@ export const ActionBar = memo(
217263 const isNoteBlock = blockType === 'note'
218264 const isInsideSubflow = parentId && ( parentType === 'loop' || parentType === 'parallel' )
219265 const cantEnable = ! isEnabled && isParentDisabled
266+ const isEffectivelyLocked = isLocked || isParentLocked
267+ const isEffectivelyDisabled = ! isEnabled || isParentDisabled
220268
221269 const snapshot = activeWorkflowId ? getLastExecutionSnapshot ( activeWorkflowId ) : null
222270 const incomingEdges = edges . filter ( ( edge ) => edge . target === blockId )
@@ -237,6 +285,9 @@ export const ActionBar = memo(
237285 dependenciesSatisfied && ! isNoteBlock && ! isInsideSubflow && ! isExecuting
238286 const isSwell = variant === 'swell'
239287 const isInline = variant === 'inline'
288+ const isPersistentInlineRun = isInline && inlineActions === 'run'
289+ const isCompactDisabledInlineRun =
290+ isPersistentInlineRun && ! isWorkflowRunning && ( isEffectivelyLocked || isEffectivelyDisabled )
240291 const firstActionId : ActionId = isNoteBlock
241292 ? 'color'
242293 : ! isInsideSubflow || isWorkflowRunning
@@ -286,8 +337,9 @@ export const ActionBar = memo(
286337
287338 return cn (
288339 actionButtonStyles ,
289- isInline && INLINE_ACTION_WIDTH_STYLES [ actionId ] ,
340+ isInline && ! isPersistentInlineRun && INLINE_ACTION_WIDTH_STYLES [ actionId ] ,
290341 isInline &&
342+ ! isPersistentInlineRun &&
291343 actionId === 'run' && [
292344 'border border-transparent' ,
293345 'group-hover/inline-action:border-[var(--border)] group-hover/inline-action:!bg-transparent' ,
@@ -391,65 +443,149 @@ export const ActionBar = memo(
391443 ]
392444 ) }
393445 >
394- { ! isNoteBlock && ( ! isInsideSubflow || isWorkflowRunning ) && (
395- < Tooltip . Root preferAbove >
396- < Tooltip . Trigger asChild >
397- < span className = { cn ( 'inline-flex' , isInline && 'group/inline-action' ) } >
398- < Button
399- variant = 'ghost'
400- aria-label = { isWorkflowRunning ? 'Stop workflow' : 'Run block' }
401- onClick = { ( e ) => {
402- e . stopPropagation ( )
403- if ( isWorkflowRunning ) {
404- handleCancelExecution ( )
405- return
406- }
407- if ( canRunFromBlock && ! disabled ) {
408- handleRunFromBlockClick ( )
409- }
410- } }
411- className = { cn ( getActionButtonStyles ( 'run' ) , isWorkflowRunning && 'group/run' ) }
412- disabled = {
413- ! isWorkflowRunning &&
414- ( disabled || ! canRunFromBlock || isLocked || isParentLocked )
415- }
416- >
417- { isWorkflowRunning ? (
418- isRunning ? (
419- < RunningActionIcon inline = { isInline } />
420- ) : (
421- < Square
422- className = { cn (
423- 'shrink-0 fill-current' ,
424- isInline ? 'size-[14px]' : 'size-[11px]'
425- ) }
426- aria-hidden = 'true'
427- strokeWidth = { 0 }
428- />
429- )
446+ { isPersistentInlineRun && isEffectivelyDisabled && (
447+ < InlineBlockStatus
448+ icon = { Ban }
449+ label = { isParentDisabled ? 'Parent container is disabled' : 'Enable block' }
450+ disabled = {
451+ isWorkflowRunning || disabled || isLocked || isParentLocked || isParentDisabled
452+ }
453+ onClick = { ( ) => collaborativeBatchToggleBlockEnabled ( [ blockId ] ) }
454+ />
455+ ) }
456+ { isPersistentInlineRun && isEffectivelyLocked && (
457+ < InlineBlockStatus
458+ icon = { Lock }
459+ label = {
460+ isParentLocked
461+ ? 'Parent container is locked'
462+ : userPermissions . canAdmin
463+ ? 'Unlock block'
464+ : 'Block is locked'
465+ }
466+ disabled = {
467+ isWorkflowRunning || disabled || isParentLocked || ! userPermissions . canAdmin
468+ }
469+ onClick = { ( ) => collaborativeBatchToggleLocked ( [ blockId ] ) }
470+ />
471+ ) }
472+ { ! isNoteBlock &&
473+ ( ! isInsideSubflow || isWorkflowRunning ) &&
474+ ( ! isInline || inlineActions !== 'menu' ) && (
475+ < Tooltip . Root preferAbove >
476+ < Tooltip . Trigger asChild >
477+ < span className = { cn ( 'inline-flex' , isInline && 'group/inline-action' ) } >
478+ { isPersistentInlineRun ? (
479+ < Chip
480+ variant = 'border'
481+ leftIcon = { isWorkflowRunning ? undefined : PlayOutline }
482+ leftAdornment = {
483+ isWorkflowRunning ? (
484+ isRunning ? (
485+ < RunningActionIcon inline />
486+ ) : (
487+ < Square
488+ className = 'size-[14px] shrink-0 fill-current'
489+ aria-hidden = 'true'
490+ strokeWidth = { 0 }
491+ />
492+ )
493+ ) : undefined
494+ }
495+ aria-label = { isWorkflowRunning ? 'Stop workflow' : 'Run block' }
496+ className = { cn (
497+ isCompactDisabledInlineRun && 'size-[30px] justify-center p-0'
498+ ) }
499+ onClick = { ( event ) => {
500+ event . stopPropagation ( )
501+ if ( isWorkflowRunning ) {
502+ handleCancelExecution ( )
503+ return
504+ }
505+ if ( canRunFromBlock && ! disabled ) {
506+ handleRunFromBlockClick ( )
507+ }
508+ } }
509+ disabled = {
510+ ! isWorkflowRunning &&
511+ ( disabled ||
512+ ! canRunFromBlock ||
513+ isEffectivelyLocked ||
514+ isEffectivelyDisabled )
515+ }
516+ >
517+ { isWorkflowRunning
518+ ? 'Stop'
519+ : isCompactDisabledInlineRun
520+ ? null
521+ : 'Run block' }
522+ </ Chip >
430523 ) : (
431- < PlayOutline className = { isInline ? INLINE_ICON_SIZE : ICON_SIZE } />
432- ) }
433- { isInline && (
434- < InlineActionLabel > { isWorkflowRunning ? 'Stop' : 'Play' } </ InlineActionLabel >
524+ < Button
525+ variant = 'ghost'
526+ aria-label = { isWorkflowRunning ? 'Stop workflow' : 'Run block' }
527+ onClick = { ( event ) => {
528+ event . stopPropagation ( )
529+ if ( isWorkflowRunning ) {
530+ handleCancelExecution ( )
531+ return
532+ }
533+ if ( canRunFromBlock && ! disabled ) {
534+ handleRunFromBlockClick ( )
535+ }
536+ } }
537+ className = { cn (
538+ getActionButtonStyles ( 'run' ) ,
539+ isWorkflowRunning && 'group/run'
540+ ) }
541+ disabled = {
542+ ! isWorkflowRunning &&
543+ ( disabled ||
544+ ! canRunFromBlock ||
545+ isEffectivelyLocked ||
546+ isEffectivelyDisabled )
547+ }
548+ >
549+ { isWorkflowRunning ? (
550+ isRunning ? (
551+ < RunningActionIcon inline = { isInline } />
552+ ) : (
553+ < Square
554+ className = { cn (
555+ 'shrink-0 fill-current' ,
556+ isInline ? 'size-[14px]' : 'size-[11px]'
557+ ) }
558+ aria-hidden = 'true'
559+ strokeWidth = { 0 }
560+ />
561+ )
562+ ) : (
563+ < PlayOutline className = { isInline ? INLINE_ICON_SIZE : ICON_SIZE } />
564+ ) }
565+ { isInline && (
566+ < InlineActionLabel >
567+ { isWorkflowRunning ? 'Stop' : 'Run block' }
568+ </ InlineActionLabel >
569+ ) }
570+ </ Button >
435571 ) }
436- </ Button >
437- </ span >
438- </ Tooltip . Trigger >
439- { ! isInline && (
440- < Tooltip . Content side = 'top' >
441- { ( ( ) => {
442- if ( isWorkflowRunning ) return 'Stop '
443- if ( isLocked || isParentLocked ) return 'Block is locked '
444- if ( disabled ) return getTooltipMessage ( 'Run' )
445- if ( isExecuting ) return 'Running...'
446- if ( ! dependenciesSatisfied ) return 'Run previous blocks first'
447- return 'Run'
448- } ) ( ) }
449- </ Tooltip . Content >
450- ) }
451- </ Tooltip . Root >
452- ) }
572+ </ span >
573+ </ Tooltip . Trigger >
574+ { ( ! isInline || isCompactDisabledInlineRun ) && (
575+ < Tooltip . Content side = 'top' >
576+ { ( ( ) => {
577+ if ( isWorkflowRunning ) return 'Stop'
578+ if ( isEffectivelyLocked ) return 'Block is locked '
579+ if ( isEffectivelyDisabled ) return 'Block is disabled '
580+ if ( disabled ) return getTooltipMessage ( 'Run' )
581+ if ( isExecuting ) return 'Running...'
582+ if ( ! dependenciesSatisfied ) return 'Run previous blocks first'
583+ return 'Run'
584+ } ) ( ) }
585+ </ Tooltip . Content >
586+ ) }
587+ </ Tooltip . Root >
588+ ) }
453589
454590 { ! isNoteBlock && ! isInline && (
455591 < Tooltip . Root preferAbove >
@@ -668,18 +804,16 @@ export const ActionBar = memo(
668804 </ Tooltip . Root >
669805 ) }
670806
671- { isInline && (
807+ { isInline && inlineActions !== 'run' && (
672808 < DropdownMenu onOpenChange = { setIsInlineMenuOpen } >
673809 < Tooltip . Root >
674810 < Tooltip . Trigger asChild >
675811 < DropdownMenuTrigger asChild >
676- < Button
677- variant = 'ghost'
678- className = 'size-[28px] rounded-md p-0 text-[var(--text-icon)] hover-hover:bg-[var(--surface-5)] hover-hover:text-[var(--text-primary)] '
812+ < Chip
813+ leftIcon = { MoreHorizontal }
814+ className = 'size-[30px] justify-center p-0'
679815 aria-label = 'Block actions'
680- >
681- < MoreHorizontal className = { INLINE_ICON_SIZE } />
682- </ Button >
816+ />
683817 </ DropdownMenuTrigger >
684818 </ Tooltip . Trigger >
685819 { ! isInlineMenuOpen && < Tooltip . Content side = 'top' > Block actions</ Tooltip . Content > }
@@ -771,6 +905,7 @@ export const ActionBar = memo(
771905 prevProps . blockType === nextProps . blockType &&
772906 prevProps . disabled === nextProps . disabled &&
773907 prevProps . variant === nextProps . variant &&
908+ prevProps . inlineActions === nextProps . inlineActions &&
774909 prevProps . isRunning === nextProps . isRunning &&
775910 prevProps . isWorkflowRunning === nextProps . isWorkflowRunning &&
776911 prevProps . noteColor === nextProps . noteColor &&
0 commit comments