Skip to content

Commit 087a21d

Browse files
committed
fix(workflow): reconcile editor and deploy experience with staging
1 parent 7fe6145 commit 087a21d

15 files changed

Lines changed: 979 additions & 787 deletions

File tree

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

Lines changed: 391 additions & 427 deletions
Large diffs are not rendered by default.

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,15 +305,29 @@ export function DeployPopover({
305305
workflowWorkspaceId ? 'YOUR_WORKSPACE_API_KEY' : 'YOUR_PERSONAL_API_KEY'
306306

307307
const getInputFormatExample = (includeStreaming = false) => {
308-
return getInputFormatExampleUtil(includeStreaming, selectedStreamingOutputs)
308+
const inputFormatExample = getInputFormatExampleUtil(includeStreaming, selectedStreamingOutputs)
309+
if (!inputFormatExample) return ''
310+
311+
const match = inputFormatExample.match(/-d\s*'([\s\S]*)'/)
312+
if (!match) {
313+
throw new Error(`Invalid workflow input example: ${inputFormatExample}`)
314+
}
315+
316+
const legacyBody = JSON.parse(match[1]) as Record<string, unknown>
317+
const { stream, selectedOutputs, ...input } = legacyBody
318+
return ` -d '${JSON.stringify({
319+
input,
320+
...(stream === true ? { stream: true } : {}),
321+
...(Array.isArray(selectedOutputs) ? { selectedOutputs } : {}),
322+
})}'`
309323
}
310324

311325
const deploymentInfo: WorkflowDeploymentInfoUI | null = (() => {
312326
if (!deploymentInfoData?.isDeployed || !workflowId) {
313327
return null
314328
}
315329

316-
const endpoint = `${getBaseUrl()}/api/workflows/${workflowId}/execute`
330+
const endpoint = `${getBaseUrl()}/api/v2/workflows/${workflowId}/execute`
317331
const inputFormatExample = getInputFormatExample(selectedStreamingOutputs.length > 0)
318332
const placeholderKey = getApiHeaderPlaceholder()
319333

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client'
22

33
import { type MouseEvent, useState } from 'react'
4-
import { Chip } from '@sim/emcn'
4+
import { Chip, toast } from '@sim/emcn'
5+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
56
import { DeployPopover } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal'
67
import {
78
useChangeDetection,
@@ -61,20 +62,64 @@ export function Deploy({ activeWorkflowId, userPermissions, disabled = false }:
6162
isEmpty ||
6263
(!isDeployed && deployReadiness.isBlocked && !deployReadiness.isSyncing)
6364

64-
const onDeployClick = async (event: MouseEvent<HTMLButtonElement>) => {
65-
if (disabled || !canDeploy || !activeWorkflowId) return
65+
const onDeployClick = async (event?: MouseEvent<HTMLButtonElement>) => {
66+
if (isRegistryLoading || isDisabled || !activeWorkflowId) return
6667

6768
if (isDeployed || isDeploymentSettling) {
69+
if (!event) setIsDeployPopoverOpen(true)
6870
return
6971
}
7072

71-
event.preventDefault()
73+
event?.preventDefault()
7274
const result = await handleDeployClick()
7375
if (result.shouldOpenModal) {
7476
setIsDeployPopoverOpen(true)
7577
}
7678
}
7779

80+
useRegisterGlobalCommands(() => [
81+
{
82+
id: 'deploy-workflow',
83+
handler: () => {
84+
/* The palette can't render a disabled state for this action yet, so a
85+
gated invocation reports the same reason the button's tooltip shows. */
86+
if (isRegistryLoading || isDisabled) {
87+
toast({ message: isRegistryLoading ? 'Workflow is still loading' : getTooltipText() })
88+
return
89+
}
90+
void onDeployClick()
91+
},
92+
},
93+
])
94+
95+
const getTooltipText = () => {
96+
if (isEmpty) {
97+
return 'Cannot deploy an empty workflow'
98+
}
99+
if (!canDeploy) {
100+
return 'Admin permissions required'
101+
}
102+
if (disabled) {
103+
return 'Workflow is locked'
104+
}
105+
if (isDeploying) {
106+
return 'Deploying...'
107+
}
108+
if (isChangeDetectionSettling) {
109+
return 'Syncing deployment state...'
110+
}
111+
if (deployReadiness.isBlocked && !isDeployed) {
112+
return deployReadiness.tooltip
113+
}
114+
if (changeDetected) {
115+
return 'Update deployment'
116+
}
117+
if (isDeployed) {
118+
return 'Active deployment'
119+
}
120+
return 'Deploy workflow'
121+
}
122+
78123
const getButtonLabel = () => {
79124
if (changeDetected) {
80125
return 'Update'

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ function ConnectionItem({
128128
blockConfig?.icon ??
129129
(connection.type === 'loop' ? Repeat : connection.type === 'parallel' ? Split : Box)
130130
const reference = `<${normalizeName(connection.name)}>`
131-
132131
return (
133132
<div className='mb-0.5 last:mb-0' ref={connectionRef}>
134133
<div
@@ -150,7 +149,7 @@ function ConnectionItem({
150149
>
151150
<WorkflowTypeTag
152151
type={connection.type}
153-
blockName={connection.name}
152+
typeLabel={connection.name}
154153
Icon={Icon}
155154
iconBgColor={blockConfig?.bgColor ?? ''}
156155
isIntegration={blockConfig?.category === 'tools'}

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

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type {
3131
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types'
3232
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
3333
import { getBlock } from '@/blocks'
34+
import { BlockTile } from '@/blocks/block-tile'
3435
import { getTileIconColorClass } from '@/blocks/icon-color'
3536
import type { BlockConfig } from '@/blocks/types'
3637
import { normalizeName } from '@/executor/constants'
@@ -164,6 +165,25 @@ const BLOCK_COLORS = {
164165
DEFAULT: '#2F55FF',
165166
} as const
166167

168+
const TagIcon: React.FC<{
169+
icon: string | React.ComponentType<{ className?: string }>
170+
color: string
171+
}> = ({ icon, color }) => (
172+
<div
173+
className='flex size-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded [&_img]:size-full'
174+
style={{ background: color }}
175+
>
176+
{typeof icon === 'string' ? (
177+
<span className={cn(getTileIconColorClass(color, true), 'font-bold text-micro')}>{icon}</span>
178+
) : (
179+
(() => {
180+
const IconComponent = icon
181+
return <IconComponent className={cn(getTileIconColorClass(color, true), 'size-[9px]')} />
182+
})()
183+
)}
184+
</div>
185+
)
186+
167187
/**
168188
* Prefix constants for special tag types.
169189
*/
@@ -381,25 +401,6 @@ const buildNestedTagTree = (tags: string[], blockName: string): NestedTag[] => {
381401
return convertToNestedTags(root, '', blockName)
382402
}
383403

384-
const TagIcon: React.FC<{
385-
icon: string | React.ComponentType<{ className?: string }>
386-
color: string
387-
}> = ({ icon, color }) => (
388-
<div
389-
className='flex size-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded [&_img]:size-full'
390-
style={{ background: color }}
391-
>
392-
{typeof icon === 'string' ? (
393-
<span className={cn(getTileIconColorClass(color, true), 'font-bold text-micro')}>{icon}</span>
394-
) : (
395-
(() => {
396-
const IconComponent = icon
397-
return <IconComponent className={cn(getTileIconColorClass(color, true), 'size-[9px]')} />
398-
})()
399-
)}
400-
</div>
401-
)
402-
403404
/**
404405
* Props for the recursive NestedTagRenderer component
405406
*/
@@ -846,7 +847,7 @@ const BlockRootTagItem: React.FC<{
846847
) : (
847848
<WorkflowTypeTag
848849
type={blockType}
849-
blockName={blockName}
850+
typeLabel={blockName}
850851
Icon={tagIcon}
851852
iconBgColor={brandColor}
852853
isIntegration={isIntegration}
@@ -1735,7 +1736,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
17351736
<>
17361737
<PopoverSection rootOnly>
17371738
<div className='flex items-center gap-1.5'>
1738-
<TagIcon icon='V' color={BLOCK_COLORS.VARIABLE} />
1739+
<BlockTile bgColor={BLOCK_COLORS.VARIABLE} fallbackLabel='V' size='sm' />
17391740
Variables
17401741
</div>
17411742
</PopoverSection>

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

Lines changed: 84 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { Button, ChipTag, cn, Loader, Tooltip, thinScrollbarClass } from '@sim/emcn'
55
import { SquareArrowUpRight } from '@sim/emcn/icons'
66
import { getWorkflowTypeAccent } from '@sim/workflow-renderer'
7+
import type { BlockRetryConfig } from '@sim/workflow-types/workflow'
78
import { isEqual } from 'es-toolkit'
89
import { useParams } from 'next/navigation'
910
import { useShallow } from 'zustand/react/shallow'
1011
import { useStoreWithEqualityFn } from 'zustand/traditional'
12+
import { isRetryEligibleBlock } from '@/lib/workflows/blocks/retry-eligibility'
1113
import {
1214
buildCanonicalIndex,
1315
isCanonicalPair,
@@ -18,6 +20,7 @@ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/provide
1820
import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar'
1921
import {
2022
AvailableData,
23+
RetrySettings,
2124
SubBlock,
2225
SubflowEditor,
2326
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components'
@@ -170,10 +173,24 @@ export function Editor() {
170173

171174
const {
172175
collaborativeSetBlockCanonicalMode,
176+
collaborativeSetBlockRetry,
173177
collaborativeUpdateBlockDescription,
174178
collaborativeUpdateBlockName,
175179
} = useCollaborativeWorkflow()
176180

181+
const supportsRetry = isRetryEligibleBlock({
182+
blockType: currentBlock?.type,
183+
category: blockConfig?.category,
184+
triggerMode,
185+
})
186+
const handleChangeRetry = useCallback(
187+
(retry: BlockRetryConfig) => {
188+
if (!currentBlockId) return
189+
collaborativeSetBlockRetry(currentBlockId, retry)
190+
},
191+
[currentBlockId, collaborativeSetBlockRetry]
192+
)
193+
177194
const [isRenaming, setIsRenaming] = useState(false)
178195
const [isEditingDescription, setIsEditingDescription] = useState(false)
179196
const [availableDataBlockId, setAvailableDataBlockId] = useState<string | null>(null)
@@ -546,65 +563,74 @@ export function Editor() {
546563
This block has no subblocks
547564
</div>
548565
) : (
549-
<BlockEditorSections blockType={currentBlock.type} subBlocks={subBlocks}>
550-
{(subBlock) => {
551-
const stableKey = getSubBlockStableKey(
552-
currentBlockId || '',
553-
subBlock,
554-
subBlockState
555-
)
556-
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id]
557-
const canonicalGroup = canonicalId
558-
? canonicalIndex.groupsById[canonicalId]
559-
: undefined
560-
const isCanonicalSwap = isCanonicalPair(canonicalGroup)
561-
const canonicalMode =
562-
canonicalGroup && isCanonicalSwap
563-
? resolveCanonicalMode(
564-
canonicalGroup,
565-
blockSubBlockValues,
566-
canonicalModeOverrides
567-
)
566+
<div className='flex flex-col gap-4'>
567+
<BlockEditorSections blockType={currentBlock.type} subBlocks={subBlocks}>
568+
{(subBlock) => {
569+
const stableKey = getSubBlockStableKey(
570+
currentBlockId || '',
571+
subBlock,
572+
subBlockState
573+
)
574+
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id]
575+
const canonicalGroup = canonicalId
576+
? canonicalIndex.groupsById[canonicalId]
568577
: undefined
569-
570-
return (
571-
<div key={stableKey} className='subblock-row'>
572-
<SubBlock
573-
blockId={currentBlockId}
574-
config={subBlock}
575-
isPreview={false}
576-
subBlockValues={subBlockState}
577-
disabled={!canEditBlock}
578-
allowExpandInPreview={false}
579-
isSearchHighlighted={
580-
activeSearchTarget?.blockId === currentBlockId &&
581-
(activeSearchTarget.subBlockId === subBlock.id ||
582-
activeSearchTarget.canonicalSubBlockId ===
583-
(subBlock.canonicalParamId ?? subBlock.id))
584-
}
585-
canonicalToggle={
586-
isCanonicalSwap && canonicalMode && canonicalId
587-
? {
588-
mode: canonicalMode,
589-
disabled: !canEditBlock,
590-
onToggle: () => {
591-
if (!currentBlockId) return
592-
const nextMode =
593-
canonicalMode === 'advanced' ? 'basic' : 'advanced'
594-
collaborativeSetBlockCanonicalMode(
595-
currentBlockId,
596-
canonicalId,
597-
nextMode
598-
)
599-
},
600-
}
601-
: undefined
602-
}
603-
/>
604-
</div>
605-
)
606-
}}
607-
</BlockEditorSections>
578+
const isCanonicalSwap = isCanonicalPair(canonicalGroup)
579+
const canonicalMode =
580+
canonicalGroup && isCanonicalSwap
581+
? resolveCanonicalMode(
582+
canonicalGroup,
583+
blockSubBlockValues,
584+
canonicalModeOverrides
585+
)
586+
: undefined
587+
588+
return (
589+
<div key={stableKey} className='subblock-row'>
590+
<SubBlock
591+
blockId={currentBlockId}
592+
config={subBlock}
593+
isPreview={false}
594+
subBlockValues={subBlockState}
595+
disabled={!canEditBlock}
596+
allowExpandInPreview={false}
597+
isSearchHighlighted={
598+
activeSearchTarget?.blockId === currentBlockId &&
599+
(activeSearchTarget.subBlockId === subBlock.id ||
600+
activeSearchTarget.canonicalSubBlockId ===
601+
(subBlock.canonicalParamId ?? subBlock.id))
602+
}
603+
canonicalToggle={
604+
isCanonicalSwap && canonicalMode && canonicalId
605+
? {
606+
mode: canonicalMode,
607+
disabled: !canEditBlock,
608+
onToggle: () => {
609+
if (!currentBlockId) return
610+
const nextMode =
611+
canonicalMode === 'advanced' ? 'basic' : 'advanced'
612+
collaborativeSetBlockCanonicalMode(
613+
currentBlockId,
614+
canonicalId,
615+
nextMode
616+
)
617+
},
618+
}
619+
: undefined
620+
}
621+
/>
622+
</div>
623+
)
624+
}}
625+
</BlockEditorSections>
626+
{supportsRetry && (
627+
<RetrySettings
628+
retry={currentBlock.retry}
629+
disabled={!canEditBlock}
630+
onChange={handleChangeRetry}
631+
/>
632+
)}
633+
</div>
608634
)}
609635
</div>
610636
</div>

0 commit comments

Comments
 (0)