Skip to content

Commit d31bdf2

Browse files
committed
refactor(logs): centralize workflow-id resolution and editor path
The logs list, its context menu, and the details panel each resolved a log's workflow id with their own copy of `workflow?.id || workflowId`, and the list disagreed with the details panel on what counts as a deleted workflow. Extract `resolveLogWorkflowId` and `workflowEditorPath` so the three surfaces cannot drift. `resolveLogWorkflowId` also returns null for Sim agent jobs, which have no workflow of their own. Only the context menu's "Open Workflow" item adopts that stricter predicate; cancel and retry keep using the previous `hasWorkflow` check so their gating is unchanged.
1 parent fbd02bc commit d31bdf2

4 files changed

Lines changed: 90 additions & 3 deletions

File tree

apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
X,
1717
} from '@sim/emcn'
1818
import type { WorkflowLogSummary } from '@/lib/api/contracts/logs'
19+
import { resolveLogWorkflowId } from '@/app/workspace/[workspaceId]/logs/utils'
1920

2021
interface LogRowContextMenuProps {
2122
isOpen: boolean
@@ -58,6 +59,12 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
5859
}: LogRowContextMenuProps) {
5960
const hasExecutionId = Boolean(log?.executionId)
6061
const hasWorkflow = Boolean(log?.workflow?.id || log?.workflowId)
62+
/**
63+
* "Open Workflow" needs a navigable target, which is stricter than
64+
* `hasWorkflow`: Sim agent jobs have no workflow of their own. Cancel/retry
65+
* keep using `hasWorkflow` so their gating is unchanged.
66+
*/
67+
const hasOpenableWorkflow = Boolean(log && resolveLogWorkflowId(log))
6168
const isCancellable =
6269
(log?.status === 'running' || log?.status === 'pending') && hasExecutionId && hasWorkflow
6370
const isRetryable = log?.status === 'failed' && hasWorkflow && log?.trigger !== 'mothership'
@@ -112,7 +119,7 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
112119
</DropdownMenuItem>
113120

114121
<DropdownMenuSeparator />
115-
<DropdownMenuItem disabled={!hasWorkflow} onSelect={onOpenWorkflow}>
122+
<DropdownMenuItem disabled={!hasOpenableWorkflow} onSelect={onOpenWorkflow}>
116123
<SquareArrowUpRight />
117124
Open Workflow
118125
</DropdownMenuItem>

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,11 @@ import {
9292
getDisplayStatus,
9393
type LogStatus,
9494
parseDuration,
95+
resolveLogWorkflowId,
9596
STATUS_CONFIG,
9697
StatusBadge,
9798
TriggerBadge,
99+
workflowEditorPath,
98100
} from './utils'
99101

100102
const LOGS_PER_PAGE = 50 as const
@@ -524,9 +526,9 @@ export default function Logs() {
524526
}, [contextMenuLog, workspaceId])
525527

526528
const handleOpenWorkflow = useCallback(() => {
527-
const wfId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId
529+
const wfId = contextMenuLog ? resolveLogWorkflowId(contextMenuLog) : null
528530
if (wfId) {
529-
window.open(`/workspace/${workspaceId}/w/${wfId}`, '_blank')
531+
window.open(workflowEditorPath(workspaceId, wfId), '_blank')
530532
}
531533
}, [contextMenuLog, workspaceId])
532534

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { resolveLogWorkflowId, workflowEditorPath } from './utils'
6+
7+
describe('resolveLogWorkflowId', () => {
8+
it('returns the nested workflow id when present', () => {
9+
expect(
10+
resolveLogWorkflowId({ trigger: 'manual', workflowId: 'wf-1', workflow: { id: 'wf-1' } })
11+
).toBe('wf-1')
12+
})
13+
14+
it('falls back to workflowId when the workflow object is absent', () => {
15+
expect(resolveLogWorkflowId({ trigger: 'api', workflowId: 'wf-2', workflow: null })).toBe(
16+
'wf-2'
17+
)
18+
})
19+
20+
it('prefers the nested workflow id over workflowId when both are set', () => {
21+
expect(
22+
resolveLogWorkflowId({ trigger: 'manual', workflowId: 'stale', workflow: { id: 'fresh' } })
23+
).toBe('fresh')
24+
})
25+
26+
it('returns null for Sim agent jobs even when a workflow id exists', () => {
27+
expect(
28+
resolveLogWorkflowId({
29+
trigger: 'mothership',
30+
workflowId: 'wf-3',
31+
workflow: { id: 'wf-3' },
32+
})
33+
).toBeNull()
34+
})
35+
36+
it('returns null for a deleted workflow (both id fields empty)', () => {
37+
expect(resolveLogWorkflowId({ trigger: 'manual', workflowId: null, workflow: null })).toBeNull()
38+
})
39+
40+
it('returns null when ids are present but empty strings', () => {
41+
expect(
42+
resolveLogWorkflowId({ trigger: 'manual', workflowId: '', workflow: { id: '' } })
43+
).toBeNull()
44+
})
45+
46+
it('treats a missing trigger as a normal workflow run', () => {
47+
expect(resolveLogWorkflowId({ workflowId: 'wf-4' })).toBe('wf-4')
48+
})
49+
})
50+
51+
describe('workflowEditorPath', () => {
52+
it('builds the workspace-scoped editor path', () => {
53+
expect(workflowEditorPath('ws-1', 'wf-1')).toBe('/workspace/ws-1/w/wf-1')
54+
})
55+
})

apps/sim/app/workspace/[workspaceId]/logs/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,29 @@ export const LOG_COLUMNS = {
1717

1818
export const DELETED_WORKFLOW_LABEL = 'Deleted Workflow'
1919

20+
/**
21+
* Resolves the workflow a log row points at, or null when there is nowhere to
22+
* navigate. Sim agent jobs have no workflow of their own, and a deleted
23+
* workflow leaves both id fields empty.
24+
*
25+
* Single source of truth for "is this log's workflow reachable" — the list row,
26+
* its context menu, and the details panel must agree, or a row can render as
27+
* "Deleted Workflow" while still linking somewhere.
28+
*/
29+
export function resolveLogWorkflowId(log: {
30+
trigger?: string | null
31+
workflowId?: string | null
32+
workflow?: { id?: string } | null
33+
}): string | null {
34+
if (log.trigger === 'mothership') return null
35+
return log.workflow?.id || log.workflowId || null
36+
}
37+
38+
/** Path to a workflow in the editor. */
39+
export function workflowEditorPath(workspaceId: string, workflowId: string): string {
40+
return `/workspace/${workspaceId}/w/${workflowId}`
41+
}
42+
2043
export type LogStatus =
2144
| 'error'
2245
| 'pending'

0 commit comments

Comments
 (0)