mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-28 03:30:10 +00:00
fix: add missing is_pinned param to ScriptsRepository.create_workflow_script() (#5318)
This commit is contained in:
parent
02bfcda30f
commit
3cb3beaa33
25 changed files with 2663 additions and 361 deletions
|
|
@ -441,31 +441,6 @@ export type WorkflowRunApiResponse = {
|
|||
workflow_title: string | null;
|
||||
};
|
||||
|
||||
export const TaskRunType = {
|
||||
TaskV1: "task_v1",
|
||||
TaskV2: "task_v2",
|
||||
WorkflowRun: "workflow_run",
|
||||
OpenaiCua: "openai_cua",
|
||||
AnthropicCua: "anthropic_cua",
|
||||
UiTars: "ui_tars",
|
||||
} as const;
|
||||
|
||||
export type TaskRunType = (typeof TaskRunType)[keyof typeof TaskRunType];
|
||||
|
||||
export type TaskRunListItem = {
|
||||
task_run_id: string;
|
||||
task_run_type: TaskRunType;
|
||||
run_id: string;
|
||||
title: string | null;
|
||||
status: string;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
created_at: string;
|
||||
workflow_permanent_id: string | null;
|
||||
script_run: boolean;
|
||||
searchable_text: string | null;
|
||||
};
|
||||
|
||||
export type WorkflowRunStatusApiResponse = {
|
||||
workflow_id: string;
|
||||
workflow_run_id: string;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Status, TaskRunListItem } from "@/api/types";
|
||||
import { Status, Task, TriggerType, WorkflowRunApiResponse } from "@/api/types";
|
||||
|
||||
type QueryReturnType = Array<TaskRunListItem>;
|
||||
type QueryReturnType = Array<Task | WorkflowRunApiResponse>;
|
||||
type UseQueryOptions = Omit<
|
||||
Parameters<typeof useQuery<QueryReturnType>>[0],
|
||||
"queryKey" | "queryFn"
|
||||
|
|
@ -13,6 +13,7 @@ type Props = {
|
|||
page?: number;
|
||||
pageSize?: number;
|
||||
statusFilters?: Array<Status>;
|
||||
triggerTypeFilters?: Array<TriggerType>;
|
||||
search?: string;
|
||||
} & UseQueryOptions;
|
||||
|
||||
|
|
@ -20,14 +21,20 @@ function useRunsQuery({
|
|||
page = 1,
|
||||
pageSize = 10,
|
||||
statusFilters,
|
||||
triggerTypeFilters,
|
||||
search,
|
||||
...queryOptions
|
||||
}: Props) {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
return useQuery<Array<TaskRunListItem>>({
|
||||
queryKey: ["runs", { statusFilters }, page, pageSize, search],
|
||||
return useQuery<Array<Task | WorkflowRunApiResponse>>({
|
||||
queryKey: [
|
||||
"runs",
|
||||
{ statusFilters, triggerTypeFilters },
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
const client = await getClient(credentialGetter);
|
||||
const params = new URLSearchParams();
|
||||
params.append("page", String(page));
|
||||
params.append("page_size", String(pageSize));
|
||||
|
|
@ -36,12 +43,16 @@ function useRunsQuery({
|
|||
params.append("status", status);
|
||||
});
|
||||
}
|
||||
if (triggerTypeFilters) {
|
||||
triggerTypeFilters.forEach((triggerType) => {
|
||||
params.append("trigger_type", triggerType);
|
||||
});
|
||||
}
|
||||
if (search) {
|
||||
params.append("search_key", search);
|
||||
}
|
||||
return client.get("/runs", { params }).then((res) => res.data);
|
||||
},
|
||||
...queryOptions,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import { LightningBoltIcon, MixerHorizontalIcon } from "@radix-ui/react-icons";
|
|||
import { Tip } from "@/components/Tip";
|
||||
import {
|
||||
Status,
|
||||
TaskRunListItem,
|
||||
TaskRunType,
|
||||
Task,
|
||||
TriggerType,
|
||||
WorkflowRunApiResponse,
|
||||
WorkflowRunStatusApiResponse,
|
||||
} from "@/api/types";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { StatusFilterDropdown } from "@/components/StatusFilterDropdown";
|
||||
import { TriggerTypeBadge } from "@/components/TriggerTypeBadge";
|
||||
import { TriggerTypeFilterDropdown } from "@/components/TriggerTypeFilterDropdown";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
|
|
@ -38,6 +41,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import * as env from "@/util/env";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -53,33 +57,21 @@ import { useParameterExpansion } from "@/routes/workflows/hooks/useParameterExpa
|
|||
import { ParameterDisplayInline } from "@/routes/workflows/components/ParameterDisplayInline";
|
||||
import { HighlightText } from "@/routes/workflows/components/HighlightText";
|
||||
|
||||
const statusValues = new Set<string>(Object.values(Status));
|
||||
function isKnownStatus(value: string): value is Status {
|
||||
return statusValues.has(value);
|
||||
}
|
||||
|
||||
function getRunNavigationPath(run: TaskRunListItem): string {
|
||||
switch (run.task_run_type) {
|
||||
case TaskRunType.WorkflowRun:
|
||||
case TaskRunType.TaskV2:
|
||||
return `/runs/${run.run_id}`;
|
||||
case TaskRunType.TaskV1:
|
||||
case TaskRunType.OpenaiCua:
|
||||
case TaskRunType.AnthropicCua:
|
||||
case TaskRunType.UiTars:
|
||||
return `/tasks/${run.run_id}/actions`;
|
||||
default:
|
||||
return `/runs/${run.run_id}`;
|
||||
}
|
||||
function isTask(run: Task | WorkflowRunApiResponse): run is Task {
|
||||
return "task_id" in run;
|
||||
}
|
||||
|
||||
function RunHistory() {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
|
||||
const itemsPerPage = searchParams.get("page_size")
|
||||
? Number(searchParams.get("page_size"))
|
||||
: 10;
|
||||
const [statusFilters, setStatusFilters] = useState<Array<Status>>([]);
|
||||
const [triggerTypeFilters, setTriggerTypeFilters] = useState<
|
||||
Array<TriggerType>
|
||||
>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebounce(search, 500);
|
||||
|
||||
|
|
@ -87,17 +79,43 @@ function RunHistory() {
|
|||
page,
|
||||
pageSize: itemsPerPage,
|
||||
statusFilters,
|
||||
triggerTypeFilters,
|
||||
search: debouncedSearch,
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: nextPageRuns } = useRunsQuery({
|
||||
page: page + 1,
|
||||
pageSize: itemsPerPage,
|
||||
statusFilters,
|
||||
search: debouncedSearch,
|
||||
enabled: runs?.length === itemsPerPage,
|
||||
});
|
||||
const { data: nextPageRuns } = useQuery<Array<Task | WorkflowRunApiResponse>>(
|
||||
{
|
||||
queryKey: [
|
||||
"runs",
|
||||
{ statusFilters, triggerTypeFilters },
|
||||
page + 1,
|
||||
itemsPerPage,
|
||||
debouncedSearch,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
const params = new URLSearchParams();
|
||||
params.append("page", String(page + 1));
|
||||
params.append("page_size", String(itemsPerPage));
|
||||
if (statusFilters) {
|
||||
statusFilters.forEach((status) => {
|
||||
params.append("status", status);
|
||||
});
|
||||
}
|
||||
if (triggerTypeFilters) {
|
||||
triggerTypeFilters.forEach((triggerType) => {
|
||||
params.append("trigger_type", triggerType);
|
||||
});
|
||||
}
|
||||
if (debouncedSearch) {
|
||||
params.append("search_key", debouncedSearch);
|
||||
}
|
||||
return client.get("/runs", { params }).then((res) => res.data);
|
||||
},
|
||||
enabled: runs && runs.length === itemsPerPage,
|
||||
},
|
||||
);
|
||||
|
||||
const isNextDisabled =
|
||||
isFetching || !nextPageRuns || nextPageRuns.length === 0;
|
||||
|
|
@ -118,8 +136,8 @@ function RunHistory() {
|
|||
|
||||
const workflowRunIds =
|
||||
runs
|
||||
?.filter((run) => run.task_run_type === TaskRunType.WorkflowRun)
|
||||
.map((run) => run.run_id)
|
||||
?.filter((run): run is WorkflowRunApiResponse => !isTask(run))
|
||||
.map((run) => run.workflow_run_id)
|
||||
.filter((id): id is string => Boolean(id)) ?? [];
|
||||
|
||||
setAutoExpandedRows(workflowRunIds);
|
||||
|
|
@ -152,137 +170,12 @@ function RunHistory() {
|
|||
if (isNextDisabled) return;
|
||||
setParamPatch({ page: String(page + 1) });
|
||||
}
|
||||
|
||||
const displayTableBody = () => {
|
||||
// Show loading skeleton
|
||||
if (isFetching) {
|
||||
return Array.from({ length: 10 }).map((_, index) => (
|
||||
<TableRow key={`row-${index}`}>
|
||||
<TableCell colSpan={6}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
));
|
||||
}
|
||||
|
||||
// No runs found
|
||||
if (runs?.length === 0) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
<div className="text-center">No runs found</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
return runs?.map((run) => {
|
||||
const executionTime = formatExecutionTime(
|
||||
run.started_at ?? run.created_at,
|
||||
run.finished_at,
|
||||
);
|
||||
const isWorkflowRun = run.task_run_type === TaskRunType.WorkflowRun;
|
||||
const isExpanded = isWorkflowRun && expandedRows.has(run.run_id);
|
||||
const navPath = getRunNavigationPath(run);
|
||||
|
||||
const titleContent = run.script_run ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tip content="Ran with code">
|
||||
<LightningBoltIcon className="text-[gold]" />
|
||||
</Tip>
|
||||
<span>{run.title ?? ""}</span>
|
||||
</div>
|
||||
) : (
|
||||
run.title ?? ""
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={run.task_run_id}>
|
||||
<TableRow
|
||||
className="cursor-pointer"
|
||||
onClick={(event) => {
|
||||
handleNavigate(event, navPath);
|
||||
}}
|
||||
>
|
||||
<TableCell className="max-w-0 truncate" title={run.run_id}>
|
||||
<HighlightText text={run.run_id} query={debouncedSearch} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="max-w-0 truncate"
|
||||
title={run.title ?? undefined}
|
||||
>
|
||||
{titleContent}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isKnownStatus(run.status) ? (
|
||||
<StatusBadge status={run.status} />
|
||||
) : (
|
||||
<span className="text-sm text-slate-400">{run.status}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="max-w-0 truncate"
|
||||
title={basicTimeFormat(run.created_at)}
|
||||
>
|
||||
{basicLocalTimeFormat(run.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-400">
|
||||
{executionTime ?? "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isWorkflowRun ? (
|
||||
<div className="flex justify-end">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleParametersExpanded(run.run_id);
|
||||
}}
|
||||
className={cn(isExpanded && "text-blue-400")}
|
||||
>
|
||||
<MixerHorizontalIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isExpanded ? "Hide Parameters" : "Show Parameters"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
) : null}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{isExpanded && run.workflow_permanent_id && (
|
||||
<TableRow key={`${run.run_id}-params`}>
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
className="bg-slate-50 dark:bg-slate-900/50"
|
||||
>
|
||||
<WorkflowRunParametersInline
|
||||
workflowPermanentId={run.workflow_permanent_id}
|
||||
workflowRunId={run.run_id}
|
||||
searchQuery={debouncedSearch}
|
||||
keywordMatchesParameter={matchesParameter}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<header>
|
||||
<h1 className="text-2xl">Run History</h1>
|
||||
</header>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<TableSearchInput
|
||||
value={search}
|
||||
onChange={(value) => {
|
||||
|
|
@ -294,10 +187,19 @@ function RunHistory() {
|
|||
placeholder="Search by run ID or parameter..."
|
||||
className="w-48 lg:w-72"
|
||||
/>
|
||||
<TriggerTypeFilterDropdown
|
||||
values={triggerTypeFilters}
|
||||
onChange={(values) => {
|
||||
setTriggerTypeFilters(values);
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("page", "1");
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
<StatusFilterDropdown
|
||||
values={statusFilters}
|
||||
onChange={(filters) => {
|
||||
setStatusFilters(filters);
|
||||
onChange={(values) => {
|
||||
setStatusFilters(values);
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("page", "1");
|
||||
setSearchParams(params, { replace: true });
|
||||
|
|
@ -320,7 +222,184 @@ function RunHistory() {
|
|||
<TableHead className="w-[8%] rounded-tr-lg text-slate-400"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>{displayTableBody()}</TableBody>
|
||||
<TableBody>
|
||||
{isFetching ? (
|
||||
Array.from({ length: 10 }).map((_, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell colSpan={6}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : runs?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
<div className="text-center">No runs found</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
runs?.map((run) => {
|
||||
if (isTask(run)) {
|
||||
const taskExecutionTime = formatExecutionTime(
|
||||
run.started_at ?? run.created_at,
|
||||
run.finished_at,
|
||||
);
|
||||
return (
|
||||
<TableRow
|
||||
key={run.task_id}
|
||||
className="cursor-pointer"
|
||||
onClick={(event) => {
|
||||
handleNavigate(event, `/tasks/${run.task_id}/actions`);
|
||||
}}
|
||||
>
|
||||
<TableCell className="max-w-0 truncate">
|
||||
{run.task_id}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-0 truncate">
|
||||
{run.url}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={run.status} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
title={basicTimeFormat(run.created_at)}
|
||||
className="max-w-0 truncate"
|
||||
>
|
||||
{basicLocalTimeFormat(run.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="truncate text-slate-400">
|
||||
{taskExecutionTime ?? "-"}
|
||||
</TableCell>
|
||||
{/* Align with workflow row's expand button column. */}
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
const workflowTitle = (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate">{run.workflow_title ?? ""}</span>
|
||||
{run.script_run === true && (
|
||||
<Tip content="Ran with code">
|
||||
<LightningBoltIcon className="text-[gold]" />
|
||||
</Tip>
|
||||
)}
|
||||
<TriggerTypeBadge triggerType={run.trigger_type} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const isExpanded = expandedRows.has(run.workflow_run_id);
|
||||
const workflowExecutionTime = formatExecutionTime(
|
||||
run.started_at ?? run.created_at,
|
||||
run.finished_at,
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={run.workflow_run_id}>
|
||||
<TableRow
|
||||
className="cursor-pointer"
|
||||
onClick={(event) => {
|
||||
handleNavigate(
|
||||
event,
|
||||
env.useNewRunsUrl
|
||||
? `/runs/${run.workflow_run_id}`
|
||||
: `/workflows/${run.workflow_permanent_id}/${run.workflow_run_id}/overview`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<TableCell
|
||||
className="max-w-0 truncate"
|
||||
title={run.workflow_run_id}
|
||||
>
|
||||
<HighlightText
|
||||
text={run.workflow_run_id}
|
||||
query={debouncedSearch}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="max-w-0 truncate"
|
||||
title={run.workflow_title ?? undefined}
|
||||
>
|
||||
{workflowTitle}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={run.status} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="max-w-0 truncate"
|
||||
title={
|
||||
run.trigger_type === TriggerType.Scheduled &&
|
||||
run.scheduled_for
|
||||
? `Scheduled: ${basicTimeFormat(run.scheduled_for)}\nStarted: ${basicTimeFormat(run.created_at)}`
|
||||
: basicTimeFormat(run.created_at)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span>{basicLocalTimeFormat(run.created_at)}</span>
|
||||
{run.trigger_type === TriggerType.Scheduled &&
|
||||
run.schedule_name && (
|
||||
<span className="text-xs text-slate-400">
|
||||
{run.schedule_name}
|
||||
{run.schedule_cron
|
||||
? ` (${run.schedule_cron})`
|
||||
: ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="truncate text-slate-400">
|
||||
{workflowExecutionTime ?? "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleParametersExpanded(
|
||||
run.workflow_run_id,
|
||||
);
|
||||
}}
|
||||
className={cn(isExpanded && "text-blue-400")}
|
||||
>
|
||||
<MixerHorizontalIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isExpanded
|
||||
? "Hide Parameters"
|
||||
: "Show Parameters"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{isExpanded && (
|
||||
<TableRow key={`${run.workflow_run_id}-params`}>
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
className="bg-slate-50 dark:bg-slate-900/50"
|
||||
>
|
||||
<WorkflowRunParametersInline
|
||||
workflowPermanentId={run.workflow_permanent_id}
|
||||
workflowRunId={run.workflow_run_id}
|
||||
searchQuery={debouncedSearch}
|
||||
keywordMatchesParameter={matchesParameter}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="relative px-3 py-3">
|
||||
<div className="absolute left-3 top-1/2 flex -translate-y-1/2 items-center gap-2 text-sm">
|
||||
|
|
@ -387,7 +466,7 @@ function WorkflowRunParametersInline({
|
|||
workflowRunId,
|
||||
searchQuery,
|
||||
keywordMatchesParameter,
|
||||
}: Readonly<WorkflowRunParametersInlineProps>) {
|
||||
}: WorkflowRunParametersInlineProps) {
|
||||
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
|
||||
|
|
|
|||
|
|
@ -1654,6 +1654,27 @@ function getElements(
|
|||
|
||||
// Create top-level edges based on next_block_label (not array order!)
|
||||
// We'll filter out conditional branch blocks below by checking conditionalNodeId
|
||||
//
|
||||
// Detect cycles by walking the next_block_label chain (not array order, since the
|
||||
// two can differ). React Flow crashes when rendering cyclic edge graphs.
|
||||
const cycleBackEdgeLabels = new Set<string>();
|
||||
{
|
||||
const visited = new Set<string>();
|
||||
let current = blocks[0]?.label ?? null;
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current);
|
||||
const block = blocksByLabel.get(current);
|
||||
if (!block) break;
|
||||
const next = block.next_block_label ?? null;
|
||||
if (next && visited.has(next)) {
|
||||
// This block's next_block_label closes a cycle — mark it
|
||||
cycleBackEdgeLabels.add(current);
|
||||
break;
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
blocks.forEach((block) => {
|
||||
const sourceNode = labelToNode.get(block.label);
|
||||
if (!sourceNode || !isWorkflowBlockNode(sourceNode)) {
|
||||
|
|
@ -1668,6 +1689,10 @@ function getElements(
|
|||
// Find target block using next_block_label
|
||||
const nextLabel = block.next_block_label;
|
||||
if (nextLabel) {
|
||||
// Skip edges that close a cycle in the next_block_label chain
|
||||
if (cycleBackEdgeLabels.has(block.label)) {
|
||||
return;
|
||||
}
|
||||
const targetNode = labelToNode.get(nextLabel);
|
||||
if (targetNode) {
|
||||
edges.push(edgeWithAddButton(sourceNode.id, targetNode.id));
|
||||
|
|
@ -1691,11 +1716,13 @@ function getElements(
|
|||
if (blocks.length === 0) {
|
||||
edges.push(defaultEdge(startNodeId, adderNodeId));
|
||||
} else {
|
||||
// Find the last top-level block (one with next_block_label === null and not in a branch)
|
||||
// There might be multiple blocks with next_block_label === null (e.g., last block in nested branches)
|
||||
// We need the one that's NOT inside any conditional
|
||||
// Find the last top-level block: one with next_block_label === null OR
|
||||
// one whose back-edge was skipped (cycle broken), and not in a branch
|
||||
const lastBlock = blocks.find((block) => {
|
||||
if (block.next_block_label !== null) {
|
||||
if (
|
||||
block.next_block_label !== null &&
|
||||
!cycleBackEdgeLabels.has(block.label)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const node = labelToNode.get(block.label);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue