Skip to content

Commit 1fc49a8

Browse files
committed
improvement(workflow): refine editor toolbar and empty states
1 parent 51d5397 commit 1fc49a8

16 files changed

Lines changed: 848 additions & 237 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx

Lines changed: 204 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { memo, useCallback, useState } from 'react'
1+
import { type ComponentType, memo, useCallback, useState } from 'react'
22
import {
33
Button,
4+
Chip,
45
cn,
56
DropdownMenu,
67
DropdownMenuContent,
@@ -66,7 +67,7 @@ const INLINE_ICON_SIZE = 'size-[16px] shrink-0'
6667
type ActionId = 'run' | 'enabled' | 'lock' | 'duplicate' | 'remove' | 'delete' | 'color'
6768

6869
const 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 &&

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/block-editor-sections/block-editor-sections.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ export function BlockEditorSections({ blockType, subBlocks, children }: BlockEdi
2929
<section
3030
key={section.id}
3131
aria-labelledby={`block-editor-section-${section.id}`}
32-
className={cn(
33-
'flex flex-col',
34-
index === 0 ? 'pt-1 pb-4' : 'border-[var(--border)] border-t pt-4 pb-4'
35-
)}
32+
className={cn('flex flex-col', index === 0 ? 'pt-1 pb-4' : 'pt-4 pb-4')}
3633
>
3734
<div className='mb-4 flex items-center gap-1 pl-0.5'>
3835
<h3

0 commit comments

Comments
 (0)